From c44d37629a6d8eae3c600426f4c83d25453e7b19 Mon Sep 17 00:00:00 2001 From: Kyriakos Z <3829551+koox00@users.noreply.github.com> Date: Wed, 23 Nov 2022 21:08:21 +0200 Subject: [PATCH] MM-46410: adds urgency on mention counts (#20999) * MM-46410: adds urgency on mention counts We have introduced priority for posts in https://github.com/mattermost/mattermost-webapp/pull/10951. We do need to color the mention badges in the webapp with a prominent color when a mention is posted in an urgent message. A thread has urgent mentions if the root post is marked as urgent, and the replies contain mentions to the user viewing the thread. This PR adds a column, urgentmentioncount, in channelmembers. Furthermore when asking for team/thread mention counts, we also return urgent mention counts for the user. Adds a new table to hold posts priorities Refactors priority out of the props and into the new table We are nilifying Metadata when post.ForPlugin(), which didn't save Priority for a post when Boards was enabled. This commit copies metadata again to the post, so metadata are reinstated. Co-authored-by: Mattermod Co-authored-by: Vishal Choudhary --- api4/post.go | 6 +- api4/resolver_channel_member_test.go | 25 +- api4/schema.graphqls | 31 +-- api4/user_test.go | 81 ++++++- app/app_iface.go | 6 +- app/channel.go | 13 +- app/notification.go | 5 +- app/opentracing/opentracing_layer.go | 52 +++- app/post.go | 84 +++++-- app/post_metadata.go | 28 ++- app/post_metadata_test.go | 44 ++-- app/post_priority.go | 34 +++ app/post_test.go | 89 +++++-- app/team.go | 18 +- app/user.go | 20 +- db/migrations/migrations.list | 4 + .../000097_create_posts_priority.down.sql | 16 ++ .../mysql/000097_create_posts_priority.up.sql | 23 ++ .../000097_create_posts_priority.down.sql | 3 + .../000097_create_posts_priority.up.sql | 9 + i18n/en.json | 12 + model/channel_member.go | 96 ++++---- model/post.go | 31 +++ model/post_metadata.go | 15 ++ model/team_member.go | 15 +- model/thread.go | 13 +- store/opentracinglayer/opentracinglayer.go | 99 +++++++- store/retrylayer/retrylayer.go | 111 ++++++++- store/retrylayer/retrylayer_test.go | 1 + store/sqlstore/channel_store.go | 224 +++++++++++++----- store/sqlstore/post_priority_store.go | 63 +++++ store/sqlstore/post_priority_store_test.go | 14 ++ store/sqlstore/post_store.go | 22 ++ store/sqlstore/store.go | 6 + store/sqlstore/thread_store.go | 132 ++++++++--- store/store.go | 16 +- store/storetest/channel_store.go | 69 +++++- store/storetest/mocks/ChannelStore.go | 45 +++- store/storetest/mocks/PostPriorityStore.go | 61 +++++ store/storetest/mocks/Store.go | 16 ++ store/storetest/mocks/ThreadStore.go | 49 ++-- store/storetest/post_priority_store.go | 72 ++++++ store/storetest/post_store.go | 25 ++ store/storetest/store.go | 3 + store/storetest/thread_store.go | 119 ++++++++-- store/storetest/user_store.go | 8 +- store/timerlayer/timerlayer.go | 91 ++++++- 47 files changed, 1676 insertions(+), 343 deletions(-) create mode 100644 app/post_priority.go create mode 100644 db/migrations/mysql/000097_create_posts_priority.down.sql create mode 100644 db/migrations/mysql/000097_create_posts_priority.up.sql create mode 100644 db/migrations/postgres/000097_create_posts_priority.down.sql create mode 100644 db/migrations/postgres/000097_create_posts_priority.up.sql create mode 100644 store/sqlstore/post_priority_store.go create mode 100644 store/sqlstore/post_priority_store_test.go create mode 100644 store/storetest/mocks/PostPriorityStore.go create mode 100644 store/storetest/post_priority_store.go diff --git a/api4/post.go b/api4/post.go index 05ca87b6c8..b4ad80d653 100644 --- a/api4/post.go +++ b/api4/post.go @@ -141,7 +141,7 @@ func createEphemeralPost(c *Context, w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) rp = model.AddPostActionCookies(rp, c.App.PostActionCookieSecret()) - rp = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, rp, true, false) + rp = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, rp, true, false, true) rp, err := c.App.SanitizePostMetadataForUser(c.AppContext, rp, c.AppContext.Session().UserId) if err != nil { c.Err = err @@ -420,7 +420,7 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) { return } - post = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, post, false, false) + post = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, post, false, false, true) post, err = c.App.SanitizePostMetadataForUser(c.AppContext, post, c.AppContext.Session().UserId) if err != nil { c.Err = err @@ -479,7 +479,7 @@ func getPostsByIds(c *Context, w http.ResponseWriter, r *http.Request) { } } - post = c.App.PreparePostForClient(c.AppContext, post, false, false) + post = c.App.PreparePostForClient(c.AppContext, post, false, false, true) post.StripActionIntegrations() posts = append(posts, post) } diff --git a/api4/resolver_channel_member_test.go b/api4/resolver_channel_member_test.go index d4278145e7..96d5da7806 100644 --- a/api4/resolver_channel_member_test.go +++ b/api4/resolver_channel_member_test.go @@ -61,17 +61,18 @@ func TestGraphQLChannelMembers(t *testing.T) { SchemeManaged bool `json:"schemeManaged"` BuiltIn bool `json:"builtIn"` } `json:"roles"` - LastViewedAt float64 `json:"lastViewedAt"` - LastUpdateAt float64 `json:"lastUpdateAt"` - MsgCount float64 `json:"msgCount"` - MentionCount float64 `json:"mentionCount"` - MentionCountRoot float64 `json:"mentionCountRoot"` - MsgCountRoot float64 `json:"msgCountRoot"` - NotifyProps model.StringMap `json:"notifyProps"` - SchemeGuest bool `json:"schemeGuest"` - SchemeUser bool `json:"schemeUser"` - SchemeAdmin bool `json:"schemeAdmin"` - Cursor string `json:"cursor"` + LastViewedAt float64 `json:"lastViewedAt"` + LastUpdateAt float64 `json:"lastUpdateAt"` + MsgCount float64 `json:"msgCount"` + MentionCount float64 `json:"mentionCount"` + MentionCountRoot float64 `json:"mentionCountRoot"` + UrgentMentionCount float64 `json:"urgentMentionCount"` + MsgCountRoot float64 `json:"msgCountRoot"` + NotifyProps model.StringMap `json:"notifyProps"` + SchemeGuest bool `json:"schemeGuest"` + SchemeUser bool `json:"schemeUser"` + SchemeAdmin bool `json:"schemeAdmin"` + Cursor string `json:"cursor"` } `json:"channelMembers"` } @@ -101,6 +102,7 @@ func TestGraphQLChannelMembers(t *testing.T) { msgCount mentionCount mentionCountRoot + urgentMentionCount msgCountRoot schemeGuest schemeUser @@ -181,6 +183,7 @@ func TestGraphQLChannelMembers(t *testing.T) { msgCount mentionCount mentionCountRoot + urgentMentionCount } } `, diff --git a/api4/schema.graphqls b/api4/schema.graphqls index 5af4d4d87c..e277832f04 100644 --- a/api4/schema.graphqls +++ b/api4/schema.graphqls @@ -69,21 +69,22 @@ type Channel { } type ChannelMember { - channel : Channel - user : User - roles : [Role]! - lastViewedAt : Float! - msgCount : Float! - mentionCount : Float! - mentionCountRoot : Float! - msgCountRoot : Float! - notifyProps : StringMap! - lastUpdateAt : Float! - schemeGuest : Boolean! - schemeUser : Boolean! - schemeAdmin : Boolean! - explicitRoles : String! - cursor: String + channel : Channel + user : User + roles : [Role]! + lastViewedAt : Float! + msgCount : Float! + mentionCount : Float! + urgentMentionCount: Float! + mentionCountRoot : Float! + msgCountRoot : Float! + notifyProps : StringMap! + lastUpdateAt : Float! + schemeGuest : Boolean! + schemeUser : Boolean! + schemeAdmin : Boolean! + explicitRoles : String! + cursor : String } # Deliberately omitting password, authData, mfaSecret. diff --git a/api4/user_test.go b/api4/user_test.go index 8185ce6c03..d269593fce 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -5720,10 +5720,12 @@ func TestUpdatePassword(t *testing.T) { } func TestGetThreadsForUser(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() + os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true") + defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY") os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") + th := Setup(t).InitBasic() + defer th.TearDown() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true @@ -5820,7 +5822,49 @@ func TestGetThreadsForUser(t *testing.T) { require.NoError(t, err) require.Len(t, uss.Threads, 1) require.Greater(t, uss.Threads[0].Post.DeleteAt, int64(0)) + }) + t.Run("isUrgent, 1 thread", func(t *testing.T) { + testCases := []struct { + featureEnabled bool + expected bool + }{ + {featureEnabled: true, expected: true}, + {featureEnabled: false, expected: false}, + } + + for _, tc := range testCases { + func() { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.PostPriority = tc.featureEnabled + cfg.FeatureFlags.PostPriority = true + }) + + client := th.Client + + rpost, resp, err := client.CreatePost(&model.Post{ + ChannelId: th.BasicChannel.Id, + Message: "testMsg", + Metadata: &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString(model.PostPriorityUrgent), + }, + }, + }) + require.NoError(t, err) + CheckCreatedStatus(t, resp) + _, resp, err = client.CreatePost(&model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id}) + require.NoError(t, err) + CheckCreatedStatus(t, resp) + + defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id) + + uss, _, err := th.Client.GetUserThreads(th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{}) + require.NoError(t, err) + require.Len(t, uss.Threads, 1) + require.Equal(t, uss.Threads[0].IsUrgent, tc.expected) + }() + } }) t.Run("paged, 30 threads", func(t *testing.T) { @@ -6515,13 +6559,19 @@ func TestThreadCounts(t *testing.T) { } func TestSingleThreadGet(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() + os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true") + defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY") os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") + + th := Setup(t).InitBasic() + defer th.TearDown() + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn + *cfg.ServiceSettings.PostPriority = true + cfg.FeatureFlags.PostPriority = true }) client := th.Client @@ -6534,7 +6584,15 @@ func TestSingleThreadGet(t *testing.T) { postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id}) // create another thread to check that we are not returning it by mistake - rpost2, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testMsg2"}) + rpost2, _ := postAndCheck(t, client, &model.Post{ + ChannelId: th.BasicChannel2.Id, + Message: "testMsg2", + Metadata: &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString(model.PostPriorityUrgent), + }, + }, + }) postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReply", RootId: rpost2.Id}) // regular user should have two threads with 3 replies total @@ -6546,9 +6604,22 @@ func TestSingleThreadGet(t *testing.T) { require.Equal(t, threads.Threads[0].PostId, tr.PostId) require.Empty(t, tr.Participants[0].Username) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.PostPriority = false + }) + tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true) require.NoError(t, err) require.NotEmpty(t, tr.Participants[0].Username) + require.Equal(t, false, tr.IsUrgent) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.PostPriority = true + }) + + tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true) + require.NoError(t, err) + require.Equal(t, true, tr.IsUrgent) } func TestMaintainUnreadMentionsInThread(t *testing.T) { diff --git a/app/app_iface.go b/app/app_iface.go index a96681d93c..093d97a035 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -716,6 +716,8 @@ type AppIface interface { GetPreferenceByCategoryForUser(userID string, category string) (model.Preferences, *model.AppError) GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) GetPrevPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string + GetPriorityForPost(postId string) (*model.PostPriority, *model.AppError) + GetPriorityForPostList(list *model.PostList) (map[string]*model.PostPriority, *model.AppError) GetPrivateChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) GetPublicChannelsByIdsForTeam(c request.CTX, teamID string, channelIDs []string) (model.ChannelList, *model.AppError) @@ -927,8 +929,8 @@ type AppIface interface { PostUpdateChannelPurposeMessage(c request.CTX, userID string, channel *model.Channel, oldChannelPurpose string, newChannelPurpose string) *model.AppError PostWithProxyAddedToImageURLs(post *model.Post) *model.Post PostWithProxyRemovedFromImageURLs(post *model.Post) *model.Post - PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost, isEditPost bool) *model.Post - PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost, isEditPost bool) *model.Post + PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post + PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post PreparePostListForClient(c request.CTX, originalList *model.PostList) *model.PostList ProcessSlackText(text string) string Publish(message *model.WebSocketEvent) diff --git a/app/channel.go b/app/channel.go index f5c864ca38..0fe00de4e6 100644 --- a/app/channel.go +++ b/app/channel.go @@ -2609,12 +2609,12 @@ func (a *App) MarkChannelAsUnreadFromPost(c request.CTX, postID string, userID s return nil, err } - unreadMentions, unreadMentionsRoot, err := a.countMentionsFromPost(c, user, post) + unreadMentions, unreadMentionsRoot, urgentMentions, err := a.countMentionsFromPost(c, user, post) if err != nil { return nil, err } - channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, true) + channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true) if nErr != nil { return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } @@ -2641,7 +2641,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID st threadId = post.Id } - unreadMentions, unreadMentionsRoot, appErr := a.countMentionsFromPost(c, user, post) + unreadMentions, unreadMentionsRoot, urgentMentions, appErr := a.countMentionsFromPost(c, user, post) if appErr != nil { return nil, appErr } @@ -2650,7 +2650,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID st // In CRT Supported Client: badge on channel only sums mentions in root posts including and below the post that was marked. // In CRT Unsupported Client: badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread. if post.RootId == "" { - channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, true) + channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true) if nErr != nil { return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } @@ -2706,7 +2706,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID st if mErr != nil { return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr) } - thread, mErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true) + thread, mErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled()) if mErr != nil { return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr) } @@ -2724,7 +2724,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID st } } - channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, 0, false) + channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, 0, 0, false) if nErr != nil { return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } @@ -2741,6 +2741,7 @@ func (a *App) sendWebSocketPostUnreadEvent(c request.CTX, channelUnread *model.C } message.Add("mention_count", channelUnread.MentionCount) message.Add("mention_count_root", channelUnread.MentionCountRoot) + message.Add("urgent_mention_count", channelUnread.UrgentMentionCount) message.Add("last_viewed_at", channelUnread.LastViewedAt) message.Add("post_id", postID) a.Publish(message) diff --git a/app/notification.go b/app/notification.go index 99c1a9a9bb..cd275134d4 100644 --- a/app/notification.go +++ b/app/notification.go @@ -305,7 +305,8 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea mentionedUsersList = append(mentionedUsersList, id) } - nErr := a.Srv().Store().Channel().IncrementMentionCount(post.ChannelId, mentionedUsersList, post.RootId == "") + nErr := a.Srv().Store().Channel().IncrementMentionCount(post.ChannelId, mentionedUsersList, post.RootId == "", post.IsUrgent()) + if nErr != nil { mlog.Warn( "Failed to update mention count", @@ -596,7 +597,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea } threadMembership = tm } - userThread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true) + userThread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled()) if err != nil { return nil, errors.Wrapf(err, "cannot get thread %q for user %q", post.RootId, uid) } diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 51a5daba92..d58f160c4b 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -8118,6 +8118,50 @@ func (a *OpenTracingAppLayer) GetPrevPostIdFromPostList(postList *model.PostList return resultVar0 } +func (a *OpenTracingAppLayer) GetPriorityForPost(postId string) (*model.PostPriority, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPriorityForPost") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetPriorityForPost(postId) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + +func (a *OpenTracingAppLayer) GetPriorityForPostList(list *model.PostList) (map[string]*model.PostPriority, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPriorityForPostList") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetPriorityForPostList(list) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetPrivateChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPrivateChannelsForTeam") @@ -13023,7 +13067,7 @@ func (a *OpenTracingAppLayer) PostWithProxyRemovedFromImageURLs(post *model.Post return resultVar0 } -func (a *OpenTracingAppLayer) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool) *model.Post { +func (a *OpenTracingAppLayer) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool, includePriority bool) *model.Post { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PreparePostForClient") @@ -13035,12 +13079,12 @@ func (a *OpenTracingAppLayer) PreparePostForClient(c request.CTX, originalPost * }() defer span.Finish() - resultVar0 := a.app.PreparePostForClient(c, originalPost, isNewPost, isEditPost) + resultVar0 := a.app.PreparePostForClient(c, originalPost, isNewPost, isEditPost, includePriority) return resultVar0 } -func (a *OpenTracingAppLayer) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool) *model.Post { +func (a *OpenTracingAppLayer) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool, includePriority bool) *model.Post { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PreparePostForClientWithEmbedsAndImages") @@ -13052,7 +13096,7 @@ func (a *OpenTracingAppLayer) PreparePostForClientWithEmbedsAndImages(c request. }() defer span.Finish() - resultVar0 := a.app.PreparePostForClientWithEmbedsAndImages(c, originalPost, isNewPost, isEditPost) + resultVar0 := a.app.PreparePostForClientWithEmbedsAndImages(c, originalPost, isNewPost, isEditPost, includePriority) return resultVar0 } diff --git a/app/post.go b/app/post.go index 301c5e9c2f..b9f1bf0ed3 100644 --- a/app/post.go +++ b/app/post.go @@ -259,7 +259,15 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel } } + if !a.isPostPriorityEnabled() && post.GetPriority() != nil { + post.Metadata.Priority = nil + } + if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { + var metadata *model.PostMetadata + if post.Metadata != nil { + metadata = post.Metadata.Copy() + } var rejectionError *model.AppError pluginContext := pluginContext(c) pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { @@ -273,8 +281,12 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel return false } if replacementPost != nil { - // the original post's metadata (if there ever was any) is lost, and will be rebuilt. post = replacementPost + if post.Metadata != nil && metadata != nil { + post.Metadata.Priority = metadata.Priority + } else { + post.Metadata = metadata + } } return true @@ -343,7 +355,9 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel // Normally, we would let the API layer call PreparePostForClient, but we do it here since it also needs // to be done when we send the post over the websocket in handlePostEvents - rpost = a.PreparePostForClient(c, rpost, true, false) + // PS: we don't want to include PostPriority from the db to avoid the replica lag, + // so we just return the one that was passed with post + rpost = a.PreparePostForClient(c, rpost, true, false, false) // Make sure poster is following the thread if *a.Config().ServiceSettings.ThreadAutoFollow && rpost.RootId != "" { @@ -515,7 +529,7 @@ func (a *App) SendEphemeralPost(c request.CTX, userID string, post *model.Post) post.GenerateActionIds() message := model.NewWebSocketEvent(model.WebsocketEventEphemeralMessage, "", post.ChannelId, userID, nil, "") - post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false) + post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false, true) post = model.AddPostActionCookies(post, a.PostActionCookieSecret()) postJSON, jsonErr := post.ToJSON() @@ -538,7 +552,7 @@ func (a *App) UpdateEphemeralPost(c request.CTX, userID string, post *model.Post post.GenerateActionIds() message := model.NewWebSocketEvent(model.WebsocketEventPostEdited, "", post.ChannelId, userID, nil, "") - post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false) + post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false, true) post = model.AddPostActionCookies(post, a.PostActionCookieSecret()) postJSON, jsonErr := post.ToJSON() if jsonErr != nil { @@ -682,7 +696,7 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) }) } - rpost = a.PreparePostForClientWithEmbedsAndImages(c, rpost, false, true) + rpost = a.PreparePostForClientWithEmbedsAndImages(c, rpost, false, true, true) // Ensure IsFollowing is nil since this updated post will be broadcast to all users // and we don't want to have to populate it for every single user and broadcast to each @@ -1705,7 +1719,7 @@ func (a *App) countThreadMentions(c request.CTX, user *model.User, post *model.P posts, nErr := a.Srv().Store().Post().GetPostsByThread(post.Id, timestamp) if nErr != nil { - return 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) + return 0, model.NewAppError("countThreadMentions", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } count := 0 @@ -1732,7 +1746,7 @@ func (a *App) countThreadMentions(c request.CTX, user *model.User, post *model.P groups, nErr := a.getGroupsAllowedForReferenceInChannel(channel, team) if nErr != nil { - return 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) + return 0, model.NewAppError("countThreadMentions", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } for _, p := range posts { @@ -1749,25 +1763,33 @@ func (a *App) countThreadMentions(c request.CTX, user *model.User, post *model.P // countMentionsFromPost returns the number of posts in the post's channel that mention the user after and including the // given post. -func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model.Post) (int, int, *model.AppError) { +func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model.Post) (int, int, int, *model.AppError) { channel, err := a.GetChannel(c, post.ChannelId) if err != nil { - return 0, 0, err + return 0, 0, 0, err } if channel.Type == model.ChannelTypeDirect { // In a DM channel, every post made by the other user is a mention count, countRoot, nErr := a.Srv().Store().Channel().CountPostsAfter(post.ChannelId, post.CreateAt-1, channel.GetOtherUserIdForDM(user.Id)) if nErr != nil { - return 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) + return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } - return count, countRoot, nil + var urgentCount int + if a.isPostPriorityEnabled() { + urgentCount, nErr = a.Srv().Store().Channel().CountUrgentPostsAfter(post.ChannelId, post.CreateAt-1, channel.GetOtherUserIdForDM(user.Id)) + if nErr != nil { + return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_urgent_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) + } + } + + return count, countRoot, urgentCount, nil } channelMember, err := a.GetChannelMember(c, channel.Id, user.Id) if err != nil { - return 0, 0, err + return 0, 0, 0, err } keywords := addMentionKeywordsForUser( @@ -1785,15 +1807,25 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model thread, err := a.GetPostThread(post.Id, model.GetPostsOptions{}, user.Id) if err != nil { - return 0, 0, err + return 0, 0, 0, err } count := 0 countRoot := 0 + urgentCount := 0 if isPostMention(user, post, keywords, thread.Posts, mentionedByThread, checkForCommentMentions) { count += 1 if post.RootId == "" { countRoot += 1 + if a.isPostPriorityEnabled() { + priority, err := a.GetPriorityForPost(post.Id) + if err != nil { + return 0, 0, 0, err + } + if priority != nil && *priority.Priority == model.PostPriorityUrgent { + urgentCount += 1 + } + } } } @@ -1807,18 +1839,32 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model PerPage: perPage, }) if err != nil { - return 0, 0, err + return 0, 0, 0, err } + mentionPostIds := make([]string, 0) for _, postID := range postList.Order { if isPostMention(user, postList.Posts[postID], keywords, postList.Posts, mentionedByThread, checkForCommentMentions) { count += 1 if postList.Posts[postID].RootId == "" { + mentionPostIds = append(mentionPostIds, postID) countRoot += 1 } } } + if a.isPostPriorityEnabled() { + priorityList, nErr := a.Srv().Store().PostPriority().GetForPosts(mentionPostIds) + if err != nil { + return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.get_priority_for_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) + } + for _, priority := range priorityList { + if *priority.Priority == model.PostPriorityUrgent { + urgentCount += 1 + } + } + } + if len(postList.Order) < perPage { break } @@ -1826,7 +1872,7 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model page += 1 } - return count, countRoot, nil + return count, countRoot, urgentCount, nil } func isCommentMention(user *model.User, post *model.Post, otherPosts map[string]*model.Post, mentionedByThread map[string]bool) bool { @@ -2025,7 +2071,7 @@ func (a *App) SetPostReminder(postID, userID string, targetTime int64) *model.Ap } message := model.NewWebSocketEvent(model.WebsocketEventEphemeralMessage, "", ephemeralPost.ChannelId, userID, nil, "") - ephemeralPost = a.PreparePostForClientWithEmbedsAndImages(request.EmptyContext(a.Log()), ephemeralPost, true, false) + ephemeralPost = a.PreparePostForClientWithEmbedsAndImages(request.EmptyContext(a.Log()), ephemeralPost, true, false, true) ephemeralPost = model.AddPostActionCookies(ephemeralPost, a.PostActionCookieSecret()) postJSON, jsonErr := ephemeralPost.ToJSON() @@ -2107,7 +2153,7 @@ func (a *App) CheckPostReminders() { func includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThreadList, userID string) (*model.TopThreadList, error) { for _, topThread := range topThreadList.Items { - topThread.Post = a.PreparePostForClientWithEmbedsAndImages(c, topThread.Post, false, false) + topThread.Post = a.PreparePostForClientWithEmbedsAndImages(c, topThread.Post, false, false, true) sanitizedPost, err := a.SanitizePostMetadataForUser(c, topThread.Post, userID) if err != nil { return nil, err @@ -2116,3 +2162,7 @@ func includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThrea } return topThreadList, nil } + +func (a *App) isPostPriorityEnabled() bool { + return a.Config().FeatureFlags.PostPriority && *a.Config().ServiceSettings.PostPriority +} diff --git a/app/post_metadata.go b/app/post_metadata.go index 9d57bf83aa..368d0803f0 100644 --- a/app/post_metadata.go +++ b/app/post_metadata.go @@ -56,11 +56,20 @@ func (a *App) PreparePostListForClient(c request.CTX, originalList *model.PostLi } for id, originalPost := range originalList.Posts { - post := a.PreparePostForClientWithEmbedsAndImages(c, originalPost, false, false) + post := a.PreparePostForClientWithEmbedsAndImages(c, originalPost, false, false, false) list.Posts[id] = post } + if a.isPostPriorityEnabled() { + priority, _ := a.GetPriorityForPostList(list) + for _, id := range list.Order { + if _, ok := priority[id]; ok { + list.Posts[id].Metadata.Priority = priority[id] + } + } + } + return list } @@ -90,7 +99,7 @@ func (a *App) OverrideIconURLIfEmoji(c request.CTX, post *model.Post) { } } -func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost, isEditPost bool) *model.Post { +func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post { post := originalPost.Clone() // Proxy image links before constructing metadata so that requests go through the proxy @@ -123,11 +132,20 @@ func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNe post.Metadata.Files = fileInfos } + if includePriority && a.isPostPriorityEnabled() && post.RootId == "" { + // Post's Priority if any + if priority, err := a.GetPriorityForPost(post.Id); err != nil { + mlog.Warn("Failed to get post priority for a post", mlog.String("post_id", post.Id), mlog.Err(err)) + } else { + post.Metadata.Priority = priority + } + } + return post } -func (a *App) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost, isEditPost bool) *model.Post { - post := a.PreparePostForClient(c, originalPost, isNewPost, isEditPost) +func (a *App) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post { + post := a.PreparePostForClient(c, originalPost, isNewPost, isEditPost, includePriority) post = a.getEmbedsAndImages(c, post, isNewPost) return post } @@ -562,7 +580,7 @@ func (a *App) getLinkMetadata(c request.CTX, requestURL string, timestamp int64, permalink = &model.Permalink{PreviewPost: model.NewPreviewPost(referencedPost, referencedTeam, referencedChannel)} } else { // referencedPost does not contain a permalink: we get its metadata - referencedPostWithMetadata := a.PreparePostForClientWithEmbedsAndImages(c, referencedPost, false, false) + referencedPostWithMetadata := a.PreparePostForClientWithEmbedsAndImages(c, referencedPost, false, false, false) permalink = &model.Permalink{PreviewPost: model.NewPreviewPost(referencedPostWithMetadata, referencedTeam, referencedChannel)} } } else { diff --git a/app/post_metadata_test.go b/app/post_metadata_test.go index 29e9ad8c4d..50e2608ebc 100644 --- a/app/post_metadata_test.go +++ b/app/post_metadata_test.go @@ -125,7 +125,7 @@ func TestPreparePostForClient(t *testing.T) { Message: message, } - clientPost := th.App.PreparePostForClient(th.Context, post, false, true) + clientPost := th.App.PreparePostForClient(th.Context, post, false, true, false) t.Run("doesn't mutate provided post", func(t *testing.T) { assert.NotEqual(t, clientPost, post, "should've returned a new post") @@ -151,7 +151,7 @@ func TestPreparePostForClient(t *testing.T) { post := th.CreatePost(th.BasicChannel) - clientPost := th.App.PreparePostForClient(th.Context, post, false, false) + clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false) assert.False(t, clientPost == post, "should've returned a new post") assert.Equal(t, clientPost, post, "shouldn't have changed any metadata") @@ -167,7 +167,7 @@ func TestPreparePostForClient(t *testing.T) { reaction3 := th.AddReactionToPost(post, th.BasicUser2, "ice_cream") post.HasReactions = true - clientPost := th.App.PreparePostForClient(th.Context, post, false, false) + clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false) assert.Len(t, clientPost.Metadata.Reactions, 3, "should've populated Reactions") assert.Equal(t, reaction1, clientPost.Metadata.Reactions[0], "first reaction is incorrect") @@ -194,7 +194,7 @@ func TestPreparePostForClient(t *testing.T) { var clientPost *model.Post assert.Eventually(t, func() bool { - clientPost = th.App.PreparePostForClient(th.Context, post, false, false) + clientPost = th.App.PreparePostForClient(th.Context, post, false, false, false) return assert.ObjectsAreEqual([]*model.FileInfo{fileInfo}, clientPost.Metadata.Files) }, time.Second, 10*time.Millisecond) @@ -230,7 +230,7 @@ func TestPreparePostForClient(t *testing.T) { th.AddReactionToPost(post, th.BasicUser2, "angry") post.HasReactions = true - clientPost := th.App.PreparePostForClient(th.Context, post, false, false) + clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false) t.Run("populates emojis", func(t *testing.T) { assert.ElementsMatch(t, []*model.Emoji{}, clientPost.Metadata.Emojis, "should've populated empty Emojis") @@ -275,7 +275,7 @@ func TestPreparePostForClient(t *testing.T) { th.AddReactionToPost(post, th.BasicUser2, "angry") post.HasReactions = true - clientPost := th.App.PreparePostForClient(th.Context, post, false, false) + clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false) t.Run("populates emojis", func(t *testing.T) { assert.ElementsMatch(t, []*model.Emoji{emoji1, emoji2, emoji3, emoji4}, clientPost.Metadata.Emojis, "should've populated post.Emojis") @@ -307,7 +307,7 @@ func TestPreparePostForClient(t *testing.T) { post.AddProp(model.PostPropsOverrideIconURL, url) post.AddProp(model.PostPropsOverrideIconEmoji, emoji) - return th.App.PreparePostForClient(th.Context, post, false, false) + return th.App.PreparePostForClient(th.Context, post, false, false, false) } emoji := "basketball" @@ -361,7 +361,7 @@ func TestPreparePostForClient(t *testing.T) { }, th.BasicChannel, false, true) require.Nil(t, err) - clientPost := th.App.PreparePostForClient(th.Context, post, false, false) + clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false) t.Run("populates image dimensions", func(t *testing.T) { imageDimensions := clientPost.Metadata.Images @@ -394,7 +394,7 @@ func TestPreparePostForClient(t *testing.T) { post.AddProp(model.PostPropsOverrideIconEmoji, true) require.NotPanics(t, func() { - _ = th.App.PreparePostForClient(th.Context, post, false, false) + _ = th.App.PreparePostForClient(th.Context, post, false, false, false) }) }) @@ -424,7 +424,7 @@ func TestPreparePostForClient(t *testing.T) { }, th.BasicChannel, false, true) require.Nil(t, err) post.Metadata.Embeds = nil - clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false) + clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false, false) // Reminder that only the first link gets an embed and dimensions @@ -459,7 +459,7 @@ func TestPreparePostForClient(t *testing.T) { }, th.BasicChannel, false, true) require.Nil(t, err) - clientPost := th.App.PreparePostForClient(th.Context, post, false, false) + clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false) firstEmbed := clientPost.Metadata.Embeds[0] ogData := firstEmbed.Data.(*opengraph.OpenGraph) @@ -502,7 +502,7 @@ func TestPreparePostForClient(t *testing.T) { }, th.BasicChannel, false, true) require.Nil(t, err) post.Metadata.Embeds = nil - clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false) + clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false, false) t.Run("populates embeds", func(t *testing.T) { assert.ElementsMatch(t, []*model.PostEmbed{ @@ -547,7 +547,7 @@ func TestPreparePostForClient(t *testing.T) { // DeleteAt isn't set on the post returned by App.DeletePost post.DeleteAt = model.GetMillis() - clientPost := th.App.PreparePostForClient(th.Context, post, false, false) + clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false) assert.NotEqual(t, nil, clientPost.Metadata, "should've populated Metadata“") assert.Equal(t, "", clientPost.Message, "should've cleaned post content") @@ -582,7 +582,7 @@ func TestPreparePostForClient(t *testing.T) { }, th.BasicChannel, false, true) require.Nil(t, err) previewPost.Metadata.Embeds = nil - clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false) + clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false, false) firstEmbed := clientPost.Metadata.Embeds[0] preview := firstEmbed.Data.(*model.PreviewPost) require.Equal(t, referencedPost.Id, preview.PostID) @@ -641,7 +641,7 @@ func TestPreparePostForClient(t *testing.T) { require.Nil(t, err) previewPost.Metadata.Embeds = nil - clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false) + clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false, false) firstEmbed := clientPost.Metadata.Embeds[0] preview := firstEmbed.Data.(*model.PreviewPost) @@ -679,7 +679,7 @@ func TestPreparePostForClient(t *testing.T) { require.Nil(t, err) previewPost.Metadata.Embeds = nil - clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false) + clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false, false) firstEmbed := clientPost.Metadata.Embeds[0] preview := firstEmbed.Data.(*model.PreviewPost) referencedPostFirstEmbed := preview.Post.Metadata.Embeds[0] @@ -726,7 +726,7 @@ func TestPreparePostForClient(t *testing.T) { require.Nil(t, err) previewPost.Metadata.Embeds = nil - clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false) + clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false, false) firstEmbed := clientPost.Metadata.Embeds[0] preview := firstEmbed.Data.(*model.PreviewPost) referencedPostMetadata := preview.Post.Metadata @@ -761,7 +761,7 @@ func TestPreparePostForClient(t *testing.T) { }, th.BasicChannel, false, true) require.Nil(t, err) - clientPost := th.App.PreparePostForClient(th.Context, previewPost, false, false) + clientPost := th.App.PreparePostForClient(th.Context, previewPost, false, false, false) firstEmbed := clientPost.Metadata.Embeds[0] preview := firstEmbed.Data.(*model.PreviewPost) require.Equal(t, referencedPost.Id, preview.PostID) @@ -770,13 +770,13 @@ func TestPreparePostForClient(t *testing.T) { *cfg.ServiceSettings.EnablePermalinkPreviews = false }) - th.App.PreparePostForClient(th.Context, previewPost, false, false) + th.App.PreparePostForClient(th.Context, previewPost, false, false, false) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePermalinkPreviews = true }) - clientPost2 := th.App.PreparePostForClient(th.Context, previewPost, false, false) + clientPost2 := th.App.PreparePostForClient(th.Context, previewPost, false, false, false) firstEmbed2 := clientPost2.Metadata.Embeds[0] preview2 := firstEmbed2.Data.(*model.PreviewPost) require.Equal(t, referencedPost.Id, preview2.PostID) @@ -828,7 +828,7 @@ func testProxyLinkedImage(t *testing.T, th *TestHelper, shouldProxy bool) { Message: fmt.Sprintf(postTemplate, imageURL), } - clientPost := th.App.PreparePostForClient(th.Context, post, false, false) + clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false) if shouldProxy { assert.Equal(t, fmt.Sprintf(postTemplate, imageURL), post.Message, "should not have mutated original post") @@ -876,7 +876,7 @@ func testProxyOpenGraphImage(t *testing.T, th *TestHelper, shouldProxy bool) { require.Nil(t, err) post.Metadata.Embeds = nil - embeds := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false).Metadata.Embeds + embeds := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false, false).Metadata.Embeds require.Len(t, embeds, 1, "should have one embed") embed := embeds[0] diff --git a/app/post_priority.go b/app/post_priority.go new file mode 100644 index 0000000000..d4efd534b8 --- /dev/null +++ b/app/post_priority.go @@ -0,0 +1,34 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "database/sql" + "net/http" + + "github.com/mattermost/mattermost-server/v6/model" +) + +func (a *App) GetPriorityForPost(postId string) (*model.PostPriority, *model.AppError) { + priority, err := a.Srv().Store().PostPriority().GetForPost(postId) + + if err != nil && err != sql.ErrNoRows { + return nil, model.NewAppError("GetPriorityForPost", "app.post_prority.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + return priority, nil +} + +func (a *App) GetPriorityForPostList(list *model.PostList) (map[string]*model.PostPriority, *model.AppError) { + priority, err := a.Srv().Store().PostPriority().GetForPosts(list.Order) + if err != nil { + return nil, model.NewAppError("GetPriorityForPost", "app.post_prority.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + priorityMap := make(map[string]*model.PostPriority) + for _, p := range priority { + priorityMap[p.PostId] = p + } + + return priorityMap, nil +} diff --git a/app/post_test.go b/app/post_test.go index 74744e2363..0b9c37a3f8 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -1598,7 +1598,7 @@ func TestCountMentionsFromPost(t *testing.T) { }, channel, false, true) require.Nil(t, err) - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, 0, count) @@ -1637,7 +1637,7 @@ func TestCountMentionsFromPost(t *testing.T) { // post1 and post3 should mention the user - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, 2, count) @@ -1676,7 +1676,7 @@ func TestCountMentionsFromPost(t *testing.T) { // post2 and post3 should mention the user - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, 2, count) @@ -1713,7 +1713,7 @@ func TestCountMentionsFromPost(t *testing.T) { }, channel, false, true) require.Nil(t, err) - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, 0, count) @@ -1755,7 +1755,7 @@ func TestCountMentionsFromPost(t *testing.T) { }, channel, false, true) require.Nil(t, err) - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, 0, count) @@ -1809,7 +1809,7 @@ func TestCountMentionsFromPost(t *testing.T) { // post2 should mention the user - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, 1, count) @@ -1863,7 +1863,7 @@ func TestCountMentionsFromPost(t *testing.T) { // post2 and post5 should mention the user - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, 2, count) @@ -1912,7 +1912,7 @@ func TestCountMentionsFromPost(t *testing.T) { // should be mentioned by post2 and post3 - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, 2, count) @@ -1942,12 +1942,12 @@ func TestCountMentionsFromPost(t *testing.T) { }, channel, false, true) require.Nil(t, err) - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, 2, count) - count, _, err = th.App.countMentionsFromPost(th.Context, user1, post1) + count, _, _, err = th.App.countMentionsFromPost(th.Context, user1, post1) assert.Nil(t, err) assert.Equal(t, 0, count) @@ -1984,7 +1984,7 @@ func TestCountMentionsFromPost(t *testing.T) { // post1 and post3 should mention the user, but we only count post3 - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post2) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post2) assert.Nil(t, err) assert.Equal(t, 1, count) @@ -2015,7 +2015,7 @@ func TestCountMentionsFromPost(t *testing.T) { // post2 should mention the user - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, 1, count) @@ -2062,7 +2062,7 @@ func TestCountMentionsFromPost(t *testing.T) { // post4 should mention the user - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post3) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post3) assert.Nil(t, err) assert.Equal(t, 1, count) @@ -2102,7 +2102,7 @@ func TestCountMentionsFromPost(t *testing.T) { // post3 should mention the user - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, 1, count) @@ -2138,11 +2138,70 @@ func TestCountMentionsFromPost(t *testing.T) { // Every post should mention the user - count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) + count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1) assert.Nil(t, err) assert.Equal(t, numPosts, count) }) + + t.Run("should count urgent mentions", func(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true") + defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY") + + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.PostPriority = true + cfg.FeatureFlags.PostPriority = true + }) + + user1 := th.BasicUser + user2 := th.BasicUser2 + + channel := th.CreateChannel(th.Context, th.BasicTeam) + th.AddUserToChannel(user2, channel) + + user2.NotifyProps[model.MentionKeysNotifyProp] = "apple" + + post1, err := th.App.CreatePost(th.Context, &model.Post{ + UserId: user1.Id, + ChannelId: channel.Id, + Message: fmt.Sprintf("@%s", user2.Username), + Metadata: &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString(model.PostPriorityUrgent), + }, + }, + }, channel, false, true) + require.Nil(t, err) + + _, err = th.App.CreatePost(th.Context, &model.Post{ + UserId: user1.Id, + ChannelId: channel.Id, + Message: fmt.Sprintf("@%s", user2.Username), + }, channel, false, true) + require.Nil(t, err) + + _, err = th.App.CreatePost(th.Context, &model.Post{ + UserId: user1.Id, + ChannelId: channel.Id, + Message: "apple", + Metadata: &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString(model.PostPriorityUrgent), + }, + }, + }, channel, false, true) + require.Nil(t, err) + + // all posts mention the user but only post1, post3 are urgent + + _, _, count, err := th.App.countMentionsFromPost(th.Context, user2, post1) + + assert.Nil(t, err) + assert.Equal(t, 2, count) + }) } func TestFillInPostProps(t *testing.T) { diff --git a/app/team.go b/app/team.go index 30020e25ef..1de3c92350 100644 --- a/app/team.go +++ b/app/team.go @@ -1741,13 +1741,14 @@ func (a *App) GetTeamsUnreadForUser(excludeTeamId string, userID string, include } else { teamIDs = append(teamIDs, id) membersMap[id] = unreads(data[i], &model.TeamUnread{ - MsgCount: 0, - MentionCount: 0, - MentionCountRoot: 0, - MsgCountRoot: 0, - ThreadCount: 0, - ThreadMentionCount: 0, - TeamId: id, + MsgCount: 0, + MentionCount: 0, + MentionCountRoot: 0, + MsgCountRoot: 0, + ThreadCount: 0, + ThreadMentionCount: 0, + ThreadUrgentMentionCount: 0, + TeamId: id, }) } } @@ -1755,7 +1756,7 @@ func (a *App) GetTeamsUnreadForUser(excludeTeamId string, userID string, include includeCollapsedThreads = includeCollapsedThreads && *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled if includeCollapsedThreads { - teamUnreads, err := a.Srv().Store().Thread().GetTeamsUnreadForUser(userID, teamIDs) + teamUnreads, err := a.Srv().Store().Thread().GetTeamsUnreadForUser(userID, teamIDs, a.isPostPriorityEnabled()) if err != nil { return nil, model.NewAppError("GetTeamsUnreadForUser", "app.team.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -1763,6 +1764,7 @@ func (a *App) GetTeamsUnreadForUser(excludeTeamId string, userID string, include if _, ok := teamUnreads[teamID]; ok { member.ThreadCount = teamUnreads[teamID].ThreadCount member.ThreadMentionCount = teamUnreads[teamID].ThreadMentionCount + member.ThreadUrgentMentionCount = teamUnreads[teamID].ThreadUrgentMentionCount } } } diff --git a/app/user.go b/app/user.go index 0c3f998c9c..206a030605 100644 --- a/app/user.go +++ b/app/user.go @@ -2391,6 +2391,10 @@ func (a *App) ConvertBotToUser(c request.CTX, bot *model.Bot, userPatch *model.U func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) { var result model.Threads var eg errgroup.Group + postPriorityIsEnabled := a.isPostPriorityEnabled() + if postPriorityIsEnabled { + options.IncludeIsUrgent = true + } if !options.ThreadsOnly { eg.Go(func() error { @@ -2427,6 +2431,18 @@ func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThre return nil }) + + if postPriorityIsEnabled { + eg.Go(func() error { + totalUnreadUrgentMentions, err := a.Srv().Store().Thread().GetTotalUnreadUrgentMentions(userID, teamID, options) + if err != nil { + return errors.Wrapf(err, "failed to count urgent mentioned threads for user id=%s", userID) + } + result.TotalUnreadUrgentMentions = totalUnreadUrgentMentions + + return nil + }) + } } if !options.TotalsOnly { @@ -2469,7 +2485,7 @@ func (a *App) GetThreadMembershipForUser(userId, threadId string) (*model.Thread } func (a *App) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, *model.AppError) { - thread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended) + thread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended, a.isPostPriorityEnabled()) if err != nil { return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -2551,7 +2567,7 @@ func (a *App) UpdateThreadFollowForUserFromChannelAdd(c request.CTX, userID, tea } message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, teamID, "", userID, nil, "") - userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true) + userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true, a.isPostPriorityEnabled()) if err != nil { var errNotFound *store.ErrNotFound diff --git a/db/migrations/migrations.list b/db/migrations/migrations.list index 641cd312af..1acb91f075 100644 --- a/db/migrations/migrations.list +++ b/db/migrations/migrations.list @@ -192,6 +192,8 @@ db/migrations/mysql/000095_remove_posts_parentid.down.sql db/migrations/mysql/000095_remove_posts_parentid.up.sql db/migrations/mysql/000096_threads_threadteamid.down.sql db/migrations/mysql/000096_threads_threadteamid.up.sql +db/migrations/mysql/000097_create_posts_priority.down.sql +db/migrations/mysql/000097_create_posts_priority.up.sql db/migrations/postgres/000001_create_teams.down.sql db/migrations/postgres/000001_create_teams.up.sql db/migrations/postgres/000002_create_team_members.down.sql @@ -384,3 +386,5 @@ db/migrations/postgres/000095_remove_posts_parentid.down.sql db/migrations/postgres/000095_remove_posts_parentid.up.sql db/migrations/postgres/000096_threads_threadteamid.down.sql db/migrations/postgres/000096_threads_threadteamid.up.sql +db/migrations/postgres/000097_create_posts_priority.down.sql +db/migrations/postgres/000097_create_posts_priority.up.sql diff --git a/db/migrations/mysql/000097_create_posts_priority.down.sql b/db/migrations/mysql/000097_create_posts_priority.down.sql new file mode 100644 index 0000000000..a0294c918a --- /dev/null +++ b/db/migrations/mysql/000097_create_posts_priority.down.sql @@ -0,0 +1,16 @@ +DROP TABLE IF EXISTS PostsPriority; + +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'ChannelMembers' + AND table_schema = DATABASE() + AND column_name = 'UrgentMentionCount' + ) > 0, + 'ALTER TABLE ChannelMembers DROP COLUMN UrgentMentionCount;', + 'SELECT 1' +)); + +PREPARE alterIfExists FROM @preparedStatement; +EXECUTE alterIfExists; +DEALLOCATE PREPARE alterIfExists; diff --git a/db/migrations/mysql/000097_create_posts_priority.up.sql b/db/migrations/mysql/000097_create_posts_priority.up.sql new file mode 100644 index 0000000000..b8b44d0544 --- /dev/null +++ b/db/migrations/mysql/000097_create_posts_priority.up.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS PostsPriority ( + PostId varchar(26) NOT NULL, + ChannelId varchar(26) NOT NULL, + Priority varchar(32) NOT NULL, + RequestedAck tinyint(1), + PersistentNotifications tinyint(1), + PRIMARY KEY (PostId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +SET @preparedStatement = (SELECT IF( + NOT EXISTS( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'ChannelMembers' + AND table_schema = DATABASE() + AND column_name = 'UrgentMentionCount' + ), + 'ALTER TABLE ChannelMembers ADD COLUMN UrgentMentionCount bigint(20);', + 'SELECT 1;' +)); + +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; diff --git a/db/migrations/postgres/000097_create_posts_priority.down.sql b/db/migrations/postgres/000097_create_posts_priority.down.sql new file mode 100644 index 0000000000..55974765fa --- /dev/null +++ b/db/migrations/postgres/000097_create_posts_priority.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS postspriority; + +ALTER TABLE channelmembers DROP COLUMN IF EXISTS urgentmentioncount; diff --git a/db/migrations/postgres/000097_create_posts_priority.up.sql b/db/migrations/postgres/000097_create_posts_priority.up.sql new file mode 100644 index 0000000000..cc22b1e96d --- /dev/null +++ b/db/migrations/postgres/000097_create_posts_priority.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS postspriority ( + postid VARCHAR(26) PRIMARY KEY, + channelid VARCHAR(26) NOT NULL, + priority VARCHAR(32) NOT NULL, + requestedack boolean, + persistentnotifications boolean +); + +ALTER TABLE channelmembers ADD COLUMN IF NOT EXISTS urgentmentioncount bigint; diff --git a/i18n/en.json b/i18n/en.json index a9612782c4..589c90973a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -4551,6 +4551,10 @@ "id": "app.channel.count_posts_since.app_error", "translation": "Unable to count messages since given date." }, + { + "id": "app.channel.count_urgent_posts_since.app_error", + "translation": "Unable to count urgent posts since given date." + }, { "id": "app.channel.create_channel.internal_error", "translation": "Unable to save channel." @@ -4679,6 +4683,10 @@ "id": "app.channel.get_pinnedpost_count.app_error", "translation": "Unable to get the channel pinned post count." }, + { + "id": "app.channel.get_priority_for_posts.app_error", + "translation": "Unable to get the priority for posts" + }, { "id": "app.channel.get_private_channels.get.app_error", "translation": "Unable to get private channels." @@ -6111,6 +6119,10 @@ "id": "app.post.update.app_error", "translation": "Unable to update the Post." }, + { + "id": "app.post_prority.get_for_post.app_error", + "translation": "Unable to get postpriority for post" + }, { "id": "app.post_reminder_dm", "translation": "Hi there, here's your reminder about this message from @{{.Username}}: {{.SiteURL}}/{{.TeamName}}/pl/{{.PostId}}" diff --git a/model/channel_member.go b/model/channel_member.go index d79695ada5..dbe0014eac 100644 --- a/model/channel_member.go +++ b/model/channel_member.go @@ -22,60 +22,64 @@ const ( ) type ChannelUnread struct { - TeamId string `json:"team_id"` - ChannelId string `json:"channel_id"` - MsgCount int64 `json:"msg_count"` - MentionCount int64 `json:"mention_count"` - MentionCountRoot int64 `json:"mention_count_root"` - MsgCountRoot int64 `json:"msg_count_root"` - NotifyProps StringMap `json:"-"` + TeamId string `json:"team_id"` + ChannelId string `json:"channel_id"` + MsgCount int64 `json:"msg_count"` + MentionCount int64 `json:"mention_count"` + MentionCountRoot int64 `json:"mention_count_root"` + UrgentMentionCount int64 `json:"urgent_mention_count"` + MsgCountRoot int64 `json:"msg_count_root"` + NotifyProps StringMap `json:"-"` } type ChannelUnreadAt struct { - TeamId string `json:"team_id"` - UserId string `json:"user_id"` - ChannelId string `json:"channel_id"` - MsgCount int64 `json:"msg_count"` - MentionCount int64 `json:"mention_count"` - MentionCountRoot int64 `json:"mention_count_root"` - MsgCountRoot int64 `json:"msg_count_root"` - LastViewedAt int64 `json:"last_viewed_at"` - NotifyProps StringMap `json:"-"` + TeamId string `json:"team_id"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + MsgCount int64 `json:"msg_count"` + MentionCount int64 `json:"mention_count"` + MentionCountRoot int64 `json:"mention_count_root"` + UrgentMentionCount int64 `json:"urgent_mention_count"` + MsgCountRoot int64 `json:"msg_count_root"` + LastViewedAt int64 `json:"last_viewed_at"` + NotifyProps StringMap `json:"-"` } type ChannelMember struct { - ChannelId string `json:"channel_id"` - UserId string `json:"user_id"` - Roles string `json:"roles"` - LastViewedAt int64 `json:"last_viewed_at"` - MsgCount int64 `json:"msg_count"` - MentionCount int64 `json:"mention_count"` - MentionCountRoot int64 `json:"mention_count_root"` - MsgCountRoot int64 `json:"msg_count_root"` - NotifyProps StringMap `json:"notify_props"` - LastUpdateAt int64 `json:"last_update_at"` - SchemeGuest bool `json:"scheme_guest"` - SchemeUser bool `json:"scheme_user"` - SchemeAdmin bool `json:"scheme_admin"` - ExplicitRoles string `json:"explicit_roles"` + ChannelId string `json:"channel_id"` + UserId string `json:"user_id"` + Roles string `json:"roles"` + LastViewedAt int64 `json:"last_viewed_at"` + MsgCount int64 `json:"msg_count"` + MentionCount int64 `json:"mention_count"` + MentionCountRoot int64 `json:"mention_count_root"` + UrgentMentionCount int64 `json:"urgent_mention_count"` + MsgCountRoot int64 `json:"msg_count_root"` + NotifyProps StringMap `json:"notify_props"` + LastUpdateAt int64 `json:"last_update_at"` + SchemeGuest bool `json:"scheme_guest"` + SchemeUser bool `json:"scheme_user"` + SchemeAdmin bool `json:"scheme_admin"` + ExplicitRoles string `json:"explicit_roles"` } func (o *ChannelMember) Auditable() map[string]interface{} { return map[string]interface{}{ - "channel_id": o.ChannelId, - "user_id": o.UserId, - "roles": o.Roles, - "last_viewed_at": o.LastViewedAt, - "msg_count": o.MsgCount, - "mention_count": o.MentionCount, - "mention_count_root": o.MentionCountRoot, - "msg_count_root": o.MsgCountRoot, - "notify_props": o.NotifyProps, - "last_update_at": o.LastUpdateAt, - "scheme_guest": o.SchemeGuest, - "scheme_user": o.SchemeUser, - "scheme_admin": o.SchemeAdmin, - "explicit_roles": o.ExplicitRoles, + "channel_id": o.ChannelId, + "user_id": o.UserId, + "roles": o.Roles, + "last_viewed_at": o.LastViewedAt, + "msg_count": o.MsgCount, + "mention_count": o.MentionCount, + "mention_count_root": o.MentionCountRoot, + "urgent_mention_count": o.UrgentMentionCount, + "msg_count_root": o.MsgCountRoot, + "notify_props": o.NotifyProps, + "last_update_at": o.LastUpdateAt, + "scheme_guest": o.SchemeGuest, + "scheme_user": o.SchemeUser, + "scheme_admin": o.SchemeAdmin, + "explicit_roles": o.ExplicitRoles, } } @@ -100,6 +104,10 @@ func (o *ChannelMember) MentionCountRoot_() float64 { return float64(o.MentionCountRoot) } +func (o *ChannelMember) UrgentMentionCount_() float64 { + return float64(o.UrgentMentionCount) +} + func (o *ChannelMember) MsgCountRoot_() float64 { return float64(o.MsgCountRoot) } diff --git a/model/post.go b/model/post.go index 3f9c5a630c..992188ac8e 100644 --- a/model/post.go +++ b/model/post.go @@ -71,6 +71,10 @@ const ( PostPropsGroupHighlightDisabled = "disable_group_highlight" PostPropsPreviewedPost = "previewed_post" + + PostPriorityUrgent = "urgent" + PostPropsRequestedAck = "requested_ack" + PostPropsPersistentNotifications = "persistent_notifications" ) const ( @@ -158,6 +162,15 @@ type PostReminder struct { UserId string `json:",omitempty"` } +type PostPriority struct { + Priority *string `json:"priority"` + RequestedAck *bool `json:"requested_ack"` + PersistentNotifications *bool `json:"persistent_notifications"` + // These fields are only used internally for interacting with DB. + PostId string `json:",omitempty"` + ChannelId string `json:",omitempty"` +} + type SearchParameter struct { Terms *string `json:"terms"` IsOrSearch *bool `json:"is_or_search"` @@ -306,6 +319,7 @@ type GetPostsOptions struct { FromCreateAt int64 // CreateAt after which to send the items Direction string // Only accepts up|down. Indicates the order in which to send the items. IncludeDeleted bool + IncludePostPriority bool } type PostCountOptions struct { @@ -770,3 +784,20 @@ func (o *Post) GetPreviewedPostProp() string { } return "" } + +func (o *Post) GetPriority() *PostPriority { + if o.Metadata != nil && o.Metadata.Priority != nil { + return o.Metadata.Priority + } + + return nil +} + +func (o *Post) IsUrgent() bool { + postPriority := o.GetPriority() + if postPriority == nil { + return false + } + + return *postPriority.Priority == PostPriorityUrgent +} diff --git a/model/post_metadata.go b/model/post_metadata.go index 6ccc1ebffc..3730d06f55 100644 --- a/model/post_metadata.go +++ b/model/post_metadata.go @@ -22,6 +22,9 @@ type PostMetadata struct { // Reactions holds reactions made to the post. Reactions []*Reaction `json:"reactions,omitempty"` + + // Reactions holds reactions made to the post. + Priority *PostPriority `json:"priority,omitempty"` } type PostImage struct { @@ -54,11 +57,23 @@ func (p *PostMetadata) Copy() *PostMetadata { reactionsCopy := make([]*Reaction, len(p.Reactions)) copy(reactionsCopy, p.Reactions) + var postPriorityCopy *PostPriority + if p.Priority != nil { + postPriorityCopy = &PostPriority{ + Priority: p.Priority.Priority, + RequestedAck: p.Priority.RequestedAck, + PersistentNotifications: p.Priority.PersistentNotifications, + PostId: p.Priority.PostId, + ChannelId: p.Priority.ChannelId, + } + } + return &PostMetadata{ Embeds: embedsCopy, Emojis: emojisCopy, Files: filesCopy, Images: imagesCopy, Reactions: reactionsCopy, + Priority: postPriorityCopy, } } diff --git a/model/team_member.go b/model/team_member.go index f38c29ed56..fbcecadd73 100644 --- a/model/team_member.go +++ b/model/team_member.go @@ -45,13 +45,14 @@ func (o *TeamMember) Auditable() map[string]interface{} { //msgp:ignore TeamUnread type TeamUnread struct { - TeamId string `json:"team_id"` - MsgCount int64 `json:"msg_count"` - MentionCount int64 `json:"mention_count"` - MentionCountRoot int64 `json:"mention_count_root"` - MsgCountRoot int64 `json:"msg_count_root"` - ThreadCount int64 `json:"thread_count"` - ThreadMentionCount int64 `json:"thread_mention_count"` + TeamId string `json:"team_id"` + MsgCount int64 `json:"msg_count"` + MentionCount int64 `json:"mention_count"` + MentionCountRoot int64 `json:"mention_count_root"` + MsgCountRoot int64 `json:"msg_count_root"` + ThreadCount int64 `json:"thread_count"` + ThreadMentionCount int64 `json:"thread_mention_count"` + ThreadUrgentMentionCount int64 `json:"thread_urgent_mention_count"` } //msgp:ignore TeamMemberForExport diff --git a/model/thread.go b/model/thread.go index 6513f41b40..ce8ebcca3c 100644 --- a/model/thread.go +++ b/model/thread.go @@ -41,14 +41,16 @@ type ThreadResponse struct { Post *Post `json:"post"` UnreadReplies int64 `json:"unread_replies"` UnreadMentions int64 `json:"unread_mentions"` + IsUrgent bool `json:"is_urgent"` DeleteAt int64 `json:"delete_at"` } type Threads struct { - Total int64 `json:"total"` - TotalUnreadThreads int64 `json:"total_unread_threads"` - TotalUnreadMentions int64 `json:"total_unread_mentions"` - Threads []*ThreadResponse `json:"threads"` + Total int64 `json:"total"` + TotalUnreadThreads int64 `json:"total_unread_threads"` + TotalUnreadMentions int64 `json:"total_unread_mentions"` + TotalUnreadUrgentMentions int64 `json:"total_unread_urgent_mentions"` + Threads []*ThreadResponse `json:"threads"` } type GetUserThreadsOpts struct { @@ -81,6 +83,9 @@ type GetUserThreadsOpts struct { // TeamOnly will only fetch threads and unreads for the specified team and excludes DMs/GMs TeamOnly bool + + // IncludeIsUrgent will return IsUrgent field as well to assert is the thread is urgent or not + IncludeIsUrgent bool } func (o *Thread) Etag() string { diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 513749fd4b..3ab5c4715d 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -37,6 +37,7 @@ type OpenTracingLayer struct { OAuthStore store.OAuthStore PluginStore store.PluginStore PostStore store.PostStore + PostPriorityStore store.PostPriorityStore PreferenceStore store.PreferenceStore ProductNoticesStore store.ProductNoticesStore ReactionStore store.ReactionStore @@ -131,6 +132,10 @@ func (s *OpenTracingLayer) Post() store.PostStore { return s.PostStore } +func (s *OpenTracingLayer) PostPriority() store.PostPriorityStore { + return s.PostPriorityStore +} + func (s *OpenTracingLayer) Preference() store.PreferenceStore { return s.PreferenceStore } @@ -301,6 +306,11 @@ type OpenTracingLayerPostStore struct { Root *OpenTracingLayer } +type OpenTracingLayerPostPriorityStore struct { + store.PostPriorityStore + Root *OpenTracingLayer +} + type OpenTracingLayerPreferenceStore struct { store.PreferenceStore Root *OpenTracingLayer @@ -702,6 +712,24 @@ func (s *OpenTracingLayerChannelStore) CountPostsAfter(channelID string, timesta return result, resultVar1, err } +func (s *OpenTracingLayerChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CountUrgentPostsAfter") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ChannelStore.CountUrgentPostsAfter(channelID, timestamp, userID) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CreateDirectChannel") @@ -1867,7 +1895,7 @@ func (s *OpenTracingLayerChannelStore) GroupSyncedChannelCount() (int64, error) return result, err } -func (s *OpenTracingLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool) error { +func (s *OpenTracingLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.IncrementMentionCount") s.Root.Store.SetContext(newCtx) @@ -1876,7 +1904,7 @@ func (s *OpenTracingLayerChannelStore) IncrementMentionCount(channelID string, u }() defer span.Finish() - err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot) + err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot, isUrgent) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -2439,7 +2467,7 @@ func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, u return result, err } -func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) { +func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateLastViewedAtPost") s.Root.Store.SetContext(newCtx) @@ -2448,7 +2476,7 @@ func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model. }() defer span.Finish() - result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, setUnreadCountRoot) + result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -6516,6 +6544,42 @@ func (s *OpenTracingLayerPostStore) Update(newPost *model.Post, oldPost *model.P return result, err } +func (s *OpenTracingLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPriorityStore.GetForPost") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.PostPriorityStore.GetForPost(postId) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + +func (s *OpenTracingLayerPostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPriorityStore.GetForPosts") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.PostPriorityStore.GetForPosts(ids) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.CleanupFlagsBatch") @@ -9872,7 +9936,7 @@ func (s *OpenTracingLayerThreadStore) GetMembershipsForUser(userId string, teamI return result, err } -func (s *OpenTracingLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) (map[string]*model.TeamUnread, error) { +func (s *OpenTracingLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTeamsUnreadForUser") s.Root.Store.SetContext(newCtx) @@ -9881,7 +9945,7 @@ func (s *OpenTracingLayerThreadStore) GetTeamsUnreadForUser(userID string, teamI }() defer span.Finish() - result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs) + result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs, includeUrgentMentionCount) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -9908,7 +9972,7 @@ func (s *OpenTracingLayerThreadStore) GetThreadFollowers(threadID string, fetchO return result, err } -func (s *OpenTracingLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, error) { +func (s *OpenTracingLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadForUser") s.Root.Store.SetContext(newCtx) @@ -9917,7 +9981,7 @@ func (s *OpenTracingLayerThreadStore) GetThreadForUser(threadMembership *model.T }() defer span.Finish() - result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended) + result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended, postPriorityIsEnabled) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -10052,6 +10116,24 @@ func (s *OpenTracingLayerThreadStore) GetTotalUnreadThreads(userId string, teamI return result, err } +func (s *OpenTracingLayerThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalUnreadUrgentMentions") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ThreadStore.GetTotalUnreadUrgentMentions(userId, teamID, opts) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MaintainMembership") @@ -12509,6 +12591,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer { newStore.OAuthStore = &OpenTracingLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore} newStore.PluginStore = &OpenTracingLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore} newStore.PostStore = &OpenTracingLayerPostStore{PostStore: childStore.Post(), Root: &newStore} + newStore.PostPriorityStore = &OpenTracingLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore} newStore.PreferenceStore = &OpenTracingLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore} newStore.ProductNoticesStore = &OpenTracingLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore} newStore.ReactionStore = &OpenTracingLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore} diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index fdf6b2c9e9..8dc782fce7 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -40,6 +40,7 @@ type RetryLayer struct { OAuthStore store.OAuthStore PluginStore store.PluginStore PostStore store.PostStore + PostPriorityStore store.PostPriorityStore PreferenceStore store.PreferenceStore ProductNoticesStore store.ProductNoticesStore ReactionStore store.ReactionStore @@ -134,6 +135,10 @@ func (s *RetryLayer) Post() store.PostStore { return s.PostStore } +func (s *RetryLayer) PostPriority() store.PostPriorityStore { + return s.PostPriorityStore +} + func (s *RetryLayer) Preference() store.PreferenceStore { return s.PreferenceStore } @@ -304,6 +309,11 @@ type RetryLayerPostStore struct { Root *RetryLayer } +type RetryLayerPostPriorityStore struct { + store.PostPriorityStore + Root *RetryLayer +} + type RetryLayerPreferenceStore struct { store.PreferenceStore Root *RetryLayer @@ -762,6 +772,27 @@ func (s *RetryLayerChannelStore) CountPostsAfter(channelID string, timestamp int } +func (s *RetryLayerChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) { + + tries := 0 + for { + result, err := s.ChannelStore.CountUrgentPostsAfter(channelID, timestamp, userID) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) { tries := 0 @@ -2112,11 +2143,11 @@ func (s *RetryLayerChannelStore) GroupSyncedChannelCount() (int64, error) { } -func (s *RetryLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool) error { +func (s *RetryLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error { tries := 0 for { - err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot) + err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot, isUrgent) if err == nil { return nil } @@ -2706,11 +2737,11 @@ func (s *RetryLayerChannelStore) UpdateLastViewedAt(channelIds []string, userID } -func (s *RetryLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) { +func (s *RetryLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) { tries := 0 for { - result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, setUnreadCountRoot) + result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot) if err == nil { return result, nil } @@ -7389,6 +7420,48 @@ func (s *RetryLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) ( } +func (s *RetryLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) { + + tries := 0 + for { + result, err := s.PostPriorityStore.GetForPost(postId) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + +func (s *RetryLayerPostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) { + + tries := 0 + for { + result, err := s.PostPriorityStore.GetForPosts(ids) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) { tries := 0 @@ -11286,11 +11359,11 @@ func (s *RetryLayerThreadStore) GetMembershipsForUser(userId string, teamID stri } -func (s *RetryLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) (map[string]*model.TeamUnread, error) { +func (s *RetryLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) { tries := 0 for { - result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs) + result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs, includeUrgentMentionCount) if err == nil { return result, nil } @@ -11328,11 +11401,11 @@ func (s *RetryLayerThreadStore) GetThreadFollowers(threadID string, fetchOnlyAct } -func (s *RetryLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, error) { +func (s *RetryLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) { tries := 0 for { - result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended) + result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended, postPriorityIsEnabled) if err == nil { return result, nil } @@ -11496,6 +11569,27 @@ func (s *RetryLayerThreadStore) GetTotalUnreadThreads(userId string, teamID stri } +func (s *RetryLayerThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) { + + tries := 0 + for { + result, err := s.ThreadStore.GetTotalUnreadUrgentMentions(userId, teamID, opts) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) { tries := 0 @@ -14261,6 +14355,7 @@ func New(childStore store.Store) *RetryLayer { newStore.OAuthStore = &RetryLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore} newStore.PluginStore = &RetryLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore} newStore.PostStore = &RetryLayerPostStore{PostStore: childStore.Post(), Root: &newStore} + newStore.PostPriorityStore = &RetryLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore} newStore.PreferenceStore = &RetryLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore} newStore.ProductNoticesStore = &RetryLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore} newStore.ReactionStore = &RetryLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore} diff --git a/store/retrylayer/retrylayer_test.go b/store/retrylayer/retrylayer_test.go index f71ce9a55d..b45bc7c561 100644 --- a/store/retrylayer/retrylayer_test.go +++ b/store/retrylayer/retrylayer_test.go @@ -54,6 +54,7 @@ func genStore() *mocks.Store { mock.On("UserTermsOfService").Return(&mocks.UserTermsOfServiceStore{}) mock.On("Webhook").Return(&mocks.WebhookStore{}) mock.On("NotifyAdmin").Return(&mocks.NotifyAdminStore{}) + mock.On("PostPriority").Return(&mocks.PostPriorityStore{}) return mock } diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 69b6835766..ab1d6c0ebb 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -41,36 +41,38 @@ type SqlChannelStore struct { } type channelMember struct { - ChannelId string - UserId string - Roles string - LastViewedAt int64 - MsgCount int64 - MentionCount int64 - NotifyProps model.StringMap - LastUpdateAt int64 - SchemeUser sql.NullBool - SchemeAdmin sql.NullBool - SchemeGuest sql.NullBool - MentionCountRoot int64 - MsgCountRoot int64 + ChannelId string + UserId string + Roles string + LastViewedAt int64 + MsgCount int64 + MentionCount int64 + UrgentMentionCount int64 + NotifyProps model.StringMap + LastUpdateAt int64 + SchemeUser sql.NullBool + SchemeAdmin sql.NullBool + SchemeGuest sql.NullBool + MentionCountRoot int64 + MsgCountRoot int64 } func NewMapFromChannelMemberModel(cm *model.ChannelMember) map[string]any { return map[string]any{ - "ChannelId": cm.ChannelId, - "UserId": cm.UserId, - "Roles": cm.ExplicitRoles, - "LastViewedAt": cm.LastViewedAt, - "MsgCount": cm.MsgCount, - "MentionCount": cm.MentionCount, - "MentionCountRoot": cm.MentionCountRoot, - "MsgCountRoot": cm.MsgCountRoot, - "NotifyProps": cm.NotifyProps, - "LastUpdateAt": cm.LastUpdateAt, - "SchemeGuest": sql.NullBool{Valid: true, Bool: cm.SchemeGuest}, - "SchemeUser": sql.NullBool{Valid: true, Bool: cm.SchemeUser}, - "SchemeAdmin": sql.NullBool{Valid: true, Bool: cm.SchemeAdmin}, + "ChannelId": cm.ChannelId, + "UserId": cm.UserId, + "Roles": cm.ExplicitRoles, + "LastViewedAt": cm.LastViewedAt, + "MsgCount": cm.MsgCount, + "MentionCount": cm.MentionCount, + "MentionCountRoot": cm.MentionCountRoot, + "UrgentMentionCount": cm.UrgentMentionCount, + "MsgCountRoot": cm.MsgCountRoot, + "NotifyProps": cm.NotifyProps, + "LastUpdateAt": cm.LastUpdateAt, + "SchemeGuest": sql.NullBool{Valid: true, Bool: cm.SchemeGuest}, + "SchemeUser": sql.NullBool{Valid: true, Bool: cm.SchemeUser}, + "SchemeAdmin": sql.NullBool{Valid: true, Bool: cm.SchemeAdmin}, } } @@ -82,6 +84,7 @@ type channelMemberWithSchemeRoles struct { MsgCount int64 MentionCount int64 MentionCountRoot int64 + UrgentMentionCount int64 NotifyProps model.StringMap LastUpdateAt int64 SchemeGuest sql.NullBool @@ -106,7 +109,7 @@ type channelMemberWithTeamWithSchemeRoles struct { type channelMemberWithTeamWithSchemeRolesList []channelMemberWithTeamWithSchemeRoles func channelMemberSliceColumns() []string { - return []string{"ChannelId", "UserId", "Roles", "LastViewedAt", "MsgCount", "MsgCountRoot", "MentionCount", "MentionCountRoot", "NotifyProps", "LastUpdateAt", "SchemeUser", "SchemeAdmin", "SchemeGuest"} + return []string{"ChannelId", "UserId", "Roles", "LastViewedAt", "MsgCount", "MsgCountRoot", "MentionCount", "MentionCountRoot", "UrgentMentionCount", "NotifyProps", "LastUpdateAt", "SchemeUser", "SchemeAdmin", "SchemeGuest"} } func channelMemberToSlice(member *model.ChannelMember) []any { @@ -119,6 +122,7 @@ func channelMemberToSlice(member *model.ChannelMember) []any { resultSlice = append(resultSlice, member.MsgCountRoot) resultSlice = append(resultSlice, member.MentionCount) resultSlice = append(resultSlice, member.MentionCountRoot) + resultSlice = append(resultSlice, member.UrgentMentionCount) resultSlice = append(resultSlice, model.MapToJSON(member.NotifyProps)) resultSlice = append(resultSlice, member.LastUpdateAt) resultSlice = append(resultSlice, member.SchemeUser) @@ -244,20 +248,21 @@ func (db channelMemberWithSchemeRoles) ToModel() *model.ChannelMember { strings.Fields(db.Roles), ) return &model.ChannelMember{ - ChannelId: db.ChannelId, - UserId: db.UserId, - Roles: strings.Join(rolesResult.roles, " "), - LastViewedAt: db.LastViewedAt, - MsgCount: db.MsgCount, - MsgCountRoot: db.MsgCountRoot, - MentionCount: db.MentionCount, - MentionCountRoot: db.MentionCountRoot, - NotifyProps: db.NotifyProps, - LastUpdateAt: db.LastUpdateAt, - SchemeAdmin: rolesResult.schemeAdmin, - SchemeUser: rolesResult.schemeUser, - SchemeGuest: rolesResult.schemeGuest, - ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "), + ChannelId: db.ChannelId, + UserId: db.UserId, + Roles: strings.Join(rolesResult.roles, " "), + LastViewedAt: db.LastViewedAt, + MsgCount: db.MsgCount, + MsgCountRoot: db.MsgCountRoot, + MentionCount: db.MentionCount, + MentionCountRoot: db.MentionCountRoot, + UrgentMentionCount: db.UrgentMentionCount, + NotifyProps: db.NotifyProps, + LastUpdateAt: db.LastUpdateAt, + SchemeAdmin: rolesResult.schemeAdmin, + SchemeUser: rolesResult.schemeUser, + SchemeGuest: rolesResult.schemeGuest, + ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "), } } @@ -307,20 +312,21 @@ func (db channelMemberWithTeamWithSchemeRoles) ToModel() *model.ChannelMemberWit ) return &model.ChannelMemberWithTeamData{ ChannelMember: model.ChannelMember{ - ChannelId: db.ChannelId, - UserId: db.UserId, - Roles: strings.Join(rolesResult.roles, " "), - LastViewedAt: db.LastViewedAt, - MsgCount: db.MsgCount, - MsgCountRoot: db.MsgCountRoot, - MentionCount: db.MentionCount, - MentionCountRoot: db.MentionCountRoot, - NotifyProps: db.NotifyProps, - LastUpdateAt: db.LastUpdateAt, - SchemeAdmin: rolesResult.schemeAdmin, - SchemeUser: rolesResult.schemeUser, - SchemeGuest: rolesResult.schemeGuest, - ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "), + ChannelId: db.ChannelId, + UserId: db.UserId, + Roles: strings.Join(rolesResult.roles, " "), + LastViewedAt: db.LastViewedAt, + MsgCount: db.MsgCount, + MsgCountRoot: db.MsgCountRoot, + MentionCount: db.MentionCount, + MentionCountRoot: db.MentionCountRoot, + UrgentMentionCount: db.UrgentMentionCount, + NotifyProps: db.NotifyProps, + LastUpdateAt: db.LastUpdateAt, + SchemeAdmin: rolesResult.schemeAdmin, + SchemeUser: rolesResult.schemeUser, + SchemeGuest: rolesResult.schemeGuest, + ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "), }, TeamName: db.TeamName, TeamDisplayName: db.TeamDisplayName, @@ -471,7 +477,20 @@ func newSqlChannelStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface func (s *SqlChannelStore) initializeQueries() { s.channelMembersForTeamWithSchemeSelectQuery = s.getQueryBuilder(). Select( - "ChannelMembers.*", + "ChannelMembers.ChannelId", + "ChannelMembers.UserId", + "ChannelMembers.Roles", + "ChannelMembers.LastViewedAt", + "ChannelMembers.MsgCount", + "ChannelMembers.MentionCount", + "ChannelMembers.MentionCountRoot", + "COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount", + "ChannelMembers.MsgCountRoot", + "ChannelMembers.NotifyProps", + "ChannelMembers.LastUpdateAt", + "ChannelMembers.SchemeUser", + "ChannelMembers.SchemeAdmin", + "ChannelMembers.SchemeGuest", "TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole", "TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole", "TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole", @@ -779,7 +798,7 @@ func (s SqlChannelStore) GetChannelUnread(channelId, userId string) (*model.Chan var unreadChannel model.ChannelUnread err := s.GetReplicaX().Get(&unreadChannel, `SELECT - Channels.TeamId TeamId, Channels.Id ChannelId, (Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount, (Channels.TotalMsgCountRoot - ChannelMembers.MsgCountRoot) MsgCountRoot, ChannelMembers.MentionCount MentionCount, ChannelMembers.MentionCountRoot MentionCountRoot, ChannelMembers.NotifyProps NotifyProps + Channels.TeamId TeamId, Channels.Id ChannelId, (Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount, (Channels.TotalMsgCountRoot - ChannelMembers.MsgCountRoot) MsgCountRoot, ChannelMembers.MentionCount MentionCount, ChannelMembers.MentionCountRoot MentionCountRoot, COALESCE(ChannelMembers.UrgentMentionCount, 0) UrgentMentionCount, ChannelMembers.NotifyProps NotifyProps FROM Channels, ChannelMembers WHERE @@ -1612,7 +1631,20 @@ func (s SqlChannelStore) GetDeleted(teamId string, offset int, limit int, userId var channelMembersWithSchemeSelectQuery = ` SELECT - ChannelMembers.*, + ChannelMembers.ChannelId, + ChannelMembers.UserId, + ChannelMembers.Roles, + ChannelMembers.LastViewedAt, + ChannelMembers.MsgCount, + ChannelMembers.MentionCount, + ChannelMembers.MentionCountRoot, + COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount, + ChannelMembers.MsgCountRoot, + ChannelMembers.NotifyProps, + ChannelMembers.LastUpdateAt, + ChannelMembers.SchemeUser, + ChannelMembers.SchemeAdmin, + ChannelMembers.SchemeGuest, COALESCE(Teams.DisplayName, '') TeamDisplayName, COALESCE(Teams.Name, '') TeamName, COALESCE(Teams.UpdateAt, 0) TeamUpdateAt, @@ -2048,7 +2080,20 @@ func (s SqlChannelStore) GetMemberForPost(postId string, userId string) (*model. var dbMember channelMemberWithSchemeRoles query := ` SELECT - ChannelMembers.*, + ChannelMembers.ChannelId, + ChannelMembers.UserId, + ChannelMembers.Roles, + ChannelMembers.LastViewedAt, + ChannelMembers.MsgCount, + ChannelMembers.MentionCount, + ChannelMembers.MentionCountRoot, + COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount, + ChannelMembers.MsgCountRoot, + ChannelMembers.NotifyProps, + ChannelMembers.LastUpdateAt, + ChannelMembers.SchemeUser, + ChannelMembers.SchemeAdmin, + ChannelMembers.SchemeGuest, TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole, TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole, TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole, @@ -2438,6 +2483,7 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) Update("ChannelMembers cm"). Set("MentionCount", 0). Set("MentionCountRoot", 0). + Set("UrgentMentionCount", 0). Set("MsgCount", sq.Expr("greatest(cm.MsgCount, c.TotalMsgCount)")). Set("MsgCountRoot", sq.Expr("greatest(cm.MsgCountRoot, c.TotalMsgCountRoot)")). Set("LastViewedAt", sq.Expr("greatest(cm.LastViewedAt, c.LastPostAt)")). @@ -2497,6 +2543,7 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) updateQuery := s.getQueryBuilder().Update("ChannelMembers"). Set("MentionCount", 0). Set("MentionCountRoot", 0). + Set("UrgentMentionCount", 0). Set("MsgCount", msgCountQuery). Set("MsgCountRoot", msgCountQueryRoot). Set("LastViewedAt", lastViewedQuery). @@ -2518,6 +2565,31 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) return times, nil } +func (s SqlChannelStore) CountUrgentPostsAfter(channelId string, timestamp int64, userId string) (int, error) { + query := s.getQueryBuilder(). + Select("count(*)"). + From("PostsPriority"). + Join("Posts ON Posts.Id = PostsPriority.PostId"). + Where(sq.And{ + sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent}, + sq.Eq{"Posts.ChannelId": channelId}, + sq.Gt{"Posts.CreateAt": timestamp}, + sq.Eq{"Posts.DeleteAt": 0}, + }) + + if userId != "" { + query = query.Where(sq.Eq{"Posts.UserId": userId}) + } + + var urgent int64 + err := s.GetReplicaX().GetBuilder(&urgent, query) + if err != nil { + return 0, errors.Wrap(err, "failed to count urgent Posts") + } + + return int(urgent), nil +} + // CountPostsAfter returns the number of posts in the given channel created after but not including the given timestamp. If given a non-empty user ID, only counts posts made by that user. func (s SqlChannelStore) CountPostsAfter(channelId string, timestamp int64, userId string) (int, int, error) { joinLeavePostTypes := []string{ @@ -2566,13 +2638,14 @@ func (s SqlChannelStore) CountPostsAfter(channelId string, timestamp int64, user if err != nil { return 0, 0, errors.Wrap(err, "failed to count root Posts") } + return int(unread), int(unreadRoot), nil } // UpdateLastViewedAtPost updates a ChannelMember as if the user last read the channel at the time of the given post. // If the provided mentionCount is -1, the given post and all posts after it are considered to be mentions. Returns // an updated model.ChannelUnreadAt that can be returned to the client. -func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) { +func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) { unreadDate := unreadPost.CreateAt - 1 unread, unreadRoot, err := s.CountPostsAfter(unreadPost.ChannelId, unreadDate, "") @@ -2587,6 +2660,7 @@ func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID s params := map[string]any{ "mentions": mentionCount, "mentionsroot": mentionCountRoot, + "urgentmentions": urgentMentionCount, "unreadcount": unread, "unreadcountroot": unreadRoot, "lastviewedat": unreadDate, @@ -2603,6 +2677,7 @@ func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID s SET MentionCount = :mentions, MentionCountRoot = :mentionsroot, + UrgentMentionCount = :urgentmentions, MsgCount = (SELECT TotalMsgCount FROM Channels WHERE ID = :channelid) - :unreadcount, MsgCountRoot = (SELECT TotalMsgCountRoot FROM Channels WHERE ID = :channelid) - :unreadcountroot, LastViewedAt = :lastviewedat, @@ -2625,6 +2700,7 @@ func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID s cm.MsgCountRoot MsgCountRoot, cm.MentionCount MentionCount, cm.MentionCountRoot MentionCountRoot, + COALESCE(cm.UrgentMentionCount, 0) UrgentMentionCount, cm.LastViewedAt LastViewedAt, cm.NotifyProps NotifyProps FROM @@ -2643,7 +2719,7 @@ func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID s return result, nil } -func (s SqlChannelStore) IncrementMentionCount(channelId string, userIDs []string, isRoot bool) error { +func (s SqlChannelStore) IncrementMentionCount(channelId string, userIDs []string, isRoot bool, isUrgent bool) error { now := model.GetMillis() rootInc := 0 @@ -2651,10 +2727,16 @@ func (s SqlChannelStore) IncrementMentionCount(channelId string, userIDs []strin rootInc = 1 } + urgentInc := 0 + if isUrgent { + urgentInc = 1 + } + sql, args, err := s.getQueryBuilder(). Update("ChannelMembers"). Set("MentionCount", sq.Expr("MentionCount + 1")). Set("MentionCountRoot", sq.Expr("MentionCountRoot + ?", rootInc)). + Set("UrgentMentionCount", sq.Expr("UrgentMentionCount + ?", urgentInc)). Set("LastUpdateAt", now). Where(sq.Eq{ "UserId": userIDs, @@ -2832,7 +2914,21 @@ func (s SqlChannelStore) GetMembersForUser(teamID string, userID string) (model. func (s SqlChannelStore) GetMembersForUserWithCursor(userID, teamID string, opts *store.ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error) { query := s.getQueryBuilder(). - Select("ChannelMembers.*", + Select( + "ChannelMembers.ChannelId", + "ChannelMembers.UserId", + "ChannelMembers.Roles", + "ChannelMembers.LastViewedAt", + "ChannelMembers.MsgCount", + "ChannelMembers.MentionCount", + "ChannelMembers.MentionCountRoot", + "COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount", + "ChannelMembers.MsgCountRoot", + "ChannelMembers.NotifyProps", + "ChannelMembers.LastUpdateAt", + "ChannelMembers.SchemeUser", + "ChannelMembers.SchemeAdmin", + "ChannelMembers.SchemeGuest", "TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole", "TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole", "TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole", @@ -3790,6 +3886,7 @@ func (s SqlChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId LastViewedAt=:LastViewedAt, MsgCount=:MsgCount, MentionCount=:MentionCount, + UrgentMentionCount=:UrgentMentionCount, NotifyProps=:NotifyProps, LastUpdateAt=:LastUpdateAt, SchemeUser=:SchemeUser, @@ -3932,6 +4029,7 @@ func (s SqlChannelStore) GetChannelMembersForExport(userId string, teamId string ChannelMembers.MsgCount, ChannelMembers.MentionCount, ChannelMembers.MentionCountRoot, + COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount, ChannelMembers.NotifyProps, ChannelMembers.LastUpdateAt, ChannelMembers.SchemeUser, @@ -3981,7 +4079,7 @@ func (s SqlChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterId s channelIds = append(channelIds, channel.Id) } query = s.getQueryBuilder(). - Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, MentionCountRoot, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest"). + Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, MentionCountRoot, COALESCE(UrgentMentionCount, 0) UrgentMentionCount, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest"). From("ChannelMembers cm"). Join("Users u ON ( u.Id = cm.UserId )"). Where(sq.And{ diff --git a/store/sqlstore/post_priority_store.go b/store/sqlstore/post_priority_store.go new file mode 100644 index 0000000000..03b302e3c5 --- /dev/null +++ b/store/sqlstore/post_priority_store.go @@ -0,0 +1,63 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package sqlstore + +import ( + "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/store" + sq "github.com/mattermost/squirrel" +) + +type SqlPostPriorityStore struct { + *SqlStore +} + +func newSqlPostPriorityStore(sqlStore *SqlStore) store.PostPriorityStore { + return &SqlPostPriorityStore{ + SqlStore: sqlStore, + } +} + +func (s *SqlPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) { + query := s.getQueryBuilder(). + Select("Priority", "RequestedAck", "PersistentNotifications"). + From("PostsPriority"). + Where(sq.Eq{"PostId": postId}) + + var postPriority model.PostPriority + err := s.GetReplicaX().GetBuilder(&postPriority, query) + if err != nil { + return nil, err + } + + return &postPriority, nil +} + +func (s *SqlPostPriorityStore) GetForPosts(postIds []string) ([]*model.PostPriority, error) { + var priority []*model.PostPriority + + perPage := 200 + for i := 0; i < len(postIds); i += perPage { + j := i + perPage + if len(postIds) < j { + j = len(postIds) + } + + query := s.getQueryBuilder(). + Select("PostId", "Priority", "RequestedAck", "PersistentNotifications"). + From("PostsPriority"). + Where(sq.Eq{"PostId": postIds[i:j]}) + + var priorityBatch []*model.PostPriority + err := s.GetReplicaX().SelectBuilder(&priority, query) + + if err != nil { + return nil, err + } + + priority = append(priority, priorityBatch...) + } + + return priority, nil +} diff --git a/store/sqlstore/post_priority_store_test.go b/store/sqlstore/post_priority_store_test.go new file mode 100644 index 0000000000..3ab4627476 --- /dev/null +++ b/store/sqlstore/post_priority_store_test.go @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package sqlstore + +import ( + "testing" + + "github.com/mattermost/mattermost-server/v6/store/storetest" +) + +func TestPostPriorityStore(t *testing.T) { + StoreTestWithSqlStore(t, storetest.TestPostPriorityStore) +} diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index 5afdddac63..b7f5979cd9 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -219,6 +219,10 @@ func (s *SqlPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, er return nil, -1, errors.Wrap(err, "update thread from posts failed") } + if err = s.savePostsPriority(transaction, posts); err != nil { + return nil, -1, errors.Wrap(err, "failed to save PostPriority") + } + if err = transaction.Commit(); err != nil { // don't need to rollback here since the transaction is already closed return posts, -1, errors.Wrap(err, "commit_transaction") @@ -2920,6 +2924,24 @@ func (s *SqlPostStore) updateThreadAfterReplyDeletion(transaction *sqlxTxWrapper return nil } +func (s *SqlPostStore) savePostsPriority(transaction *sqlxTxWrapper, posts []*model.Post) error { + for _, post := range posts { + if post.GetPriority() != nil { + postPriority := &model.PostPriority{ + PostId: post.Id, + ChannelId: post.ChannelId, + Priority: post.Metadata.Priority.Priority, + RequestedAck: post.Metadata.Priority.RequestedAck, + PersistentNotifications: post.Metadata.Priority.PersistentNotifications, + } + if _, err := transaction.NamedExec(`INSERT INTO PostsPriority (PostId, ChannelId, Priority, RequestedAck, PersistentNotifications) VALUES (:PostId, :ChannelId, :Priority, :RequestedAck, :PersistentNotifications)`, postPriority); err != nil { + return err + } + } + } + return nil +} + func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts []*model.Post) error { postsByRoot := map[string][]*model.Post{} var rootIds []string diff --git a/store/sqlstore/store.go b/store/sqlstore/store.go index 2730ece664..0c5aad077f 100644 --- a/store/sqlstore/store.go +++ b/store/sqlstore/store.go @@ -109,6 +109,7 @@ type SqlStoreStores struct { linkMetadata store.LinkMetadataStore sharedchannel store.SharedChannelStore notifyAdmin store.NotifyAdminStore + postPriority store.PostPriorityStore } type SqlStore struct { @@ -214,6 +215,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS store.stores.group = newSqlGroupStore(store) store.stores.productNotices = newSqlProductNoticesStore(store) store.stores.notifyAdmin = newSqlNotifyAdminStore(store) + store.stores.postPriority = newSqlPostPriorityStore(store) store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures() @@ -955,6 +957,10 @@ func (ss *SqlStore) SharedChannel() store.SharedChannelStore { return ss.stores.sharedchannel } +func (ss *SqlStore) PostPriority() store.PostPriorityStore { + return ss.stores.postPriority +} + func (ss *SqlStore) DropAllTables() { if ss.DriverName() == model.DatabaseDriverPostgres { ss.masterX.Exec(`DO diff --git a/store/sqlstore/thread_store.go b/store/sqlstore/thread_store.go index e314e82093..0de9c5db45 100644 --- a/store/sqlstore/thread_store.go +++ b/store/sqlstore/thread_store.go @@ -7,11 +7,11 @@ import ( "context" "database/sql" "strconv" - "sync" "time" sq "github.com/mattermost/squirrel" "github.com/pkg/errors" + "golang.org/x/sync/errgroup" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/store" @@ -30,6 +30,7 @@ type JoinedThread struct { Participants model.StringArray ThreadDeleteAt int64 TeamId string + IsUrgent bool model.Post } @@ -51,6 +52,7 @@ func (thread *JoinedThread) toThreadResponse(users map[string]*model.User) *mode Participants: threadParticipants, Post: thread.Post.ToNilIfInvalid(), DeleteAt: thread.ThreadDeleteAt, + IsUrgent: thread.IsUrgent, } } @@ -213,6 +215,46 @@ func (s *SqlThreadStore) GetTotalUnreadMentions(userId, teamId string, opts mode return totalUnreadMentions, nil } +// GetTotalUnreadUrgentMentions counts the number of unread mentions for the given user, optionally +// constrained to the given team + DMs/GMs. +func (s *SqlThreadStore) GetTotalUnreadUrgentMentions(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) { + var totalUnreadUrgentMentions int64 + + query := s.getQueryBuilder(). + Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0)"). + From("ThreadMemberships"). + Join("PostsPriority ON PostsPriority.PostId = ThreadMemberships.PostId"). + Where(sq.Eq{ + "ThreadMemberships.UserId": userId, + "ThreadMemberships.Following": true, + "PostsPriority.Priority": model.PostPriorityUrgent, + }) + + if teamId != "" || !opts.Deleted { + query = query.Join("Threads ON Threads.PostId = ThreadMemberships.PostId") + } + + if teamId != "" { + query = query. + Where(sq.Or{ + sq.Eq{"Threads.ThreadTeamId": teamId}, + sq.Eq{"Threads.ThreadTeamId": ""}, + }) + } + + if !opts.Deleted { + query = query. + Where(sq.Eq{"COALESCE(Threads.ThreadDeleteAt, 0)": 0}) + } + + err := s.GetReplicaX().GetBuilder(&totalUnreadUrgentMentions, query) + if err != nil { + return 0, errors.Wrapf(err, "failed to count unread urgent mentions for user id=%s", userId) + } + + return totalUnreadUrgentMentions, nil +} + func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) { pageSize := uint64(30) if opts.PageSize != 0 { @@ -243,6 +285,17 @@ func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.Get Where(sq.Eq{"ThreadMemberships.UserId": userId}). Where(sq.Eq{"ThreadMemberships.Following": true}) + if opts.IncludeIsUrgent { + urgencyCase := sq. + Case(). + When(sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent}, "true"). + Else("false") + + query = query. + Column(sq.Alias(urgencyCase, "IsUrgent")). + LeftJoin("PostsPriority ON PostsPriority.PostId = Threads.PostId") + } + // If a team is specified, constrain to channels in that team or DMs/GMs without // a team at all. if teamId != "" { @@ -322,7 +375,7 @@ func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.Get // GetTeamsUnreadForUser returns the total unread threads and unread mentions // for a user from all teams. -func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) (map[string]*model.TeamUnread, error) { +func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) { fetchConditions := sq.And{ sq.Eq{"ThreadMemberships.UserId": userID}, sq.Eq{"ThreadMemberships.Following": true}, @@ -330,8 +383,7 @@ func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) sq.Eq{"COALESCE(Threads.ThreadDeleteAt, 0)": 0}, } - var wg sync.WaitGroup - var err1, err2 error + var eg errgroup.Group unreadThreads := []struct { Count int64 @@ -341,13 +393,15 @@ func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) Count int64 TeamId string }{} + unreadUrgentMentions := []struct { + Count int64 + TeamId string + }{} // Running these concurrently hasn't shown any major downside // than running them serially. So using a bit of perf boost. // In any case, they will be replaced by computed columns later. - wg.Add(1) - go func() { - defer wg.Done() + eg.Go(func() error { repliesQuery := s.getQueryBuilder(). Select("COUNT(Threads.PostId) AS Count, ThreadTeamId AS TeamId"). From("Threads"). @@ -356,15 +410,10 @@ func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) Where("Threads.LastReplyAt > ThreadMemberships.LastViewed"). GroupBy("Threads.ThreadTeamId") - err := s.GetReplicaX().SelectBuilder(&unreadThreads, repliesQuery) - if err != nil { - err1 = errors.Wrap(err, "failed to get total unread threads") - } - }() + return errors.Wrap(s.GetReplicaX().SelectBuilder(&unreadThreads, repliesQuery), "failed to get total unread threads") + }) - wg.Add(1) - go func() { - defer wg.Done() + eg.Go(func() error { mentionsQuery := s.getQueryBuilder(). Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0) AS Count, ThreadTeamId AS TeamId"). From("ThreadMemberships"). @@ -372,20 +421,27 @@ func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) Where(fetchConditions). GroupBy("Threads.ThreadTeamId") - err := s.GetReplicaX().SelectBuilder(&unreadMentions, mentionsQuery) - if err != nil { - err2 = errors.Wrap(err, "failed to get total unread mentions") - } - }() + return errors.Wrap(s.GetReplicaX().SelectBuilder(&unreadMentions, mentionsQuery), "failed to get total unread mentions") + }) + + if includeUrgentMentionCount { + eg.Go(func() error { + urgentMentionsQuery := s.getQueryBuilder(). + Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0) AS Count, ThreadTeamId AS TeamId"). + From("ThreadMemberships"). + LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId"). + Join("PostsPriority ON PostsPriority.PostId = ThreadMemberships.PostId"). + Where(sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent}). + Where(fetchConditions). + GroupBy("Threads.ThreadTeamId") + + return errors.Wrap(s.GetReplicaX().SelectBuilder(&unreadUrgentMentions, urgentMentionsQuery), "failed to get total unread urgent mentions") + }) + } // Wait for them to be over - wg.Wait() - - if err1 != nil { - return nil, err1 - } - if err2 != nil { - return nil, err2 + if err := eg.Wait(); err != nil { + return nil, err } res := make(map[string]*model.TeamUnread) @@ -405,6 +461,15 @@ func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) } } } + for _, item := range unreadUrgentMentions { + if _, ok := res[item.TeamId]; ok { + res[item.TeamId].ThreadUrgentMentionCount = item.Count + } else { + res[item.TeamId] = &model.TeamUnread{ + ThreadUrgentMentionCount: item.Count, + } + } + } return res, nil } @@ -436,7 +501,7 @@ func (s *SqlThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive boo return users, nil } -func (s *SqlThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, error) { +func (s *SqlThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended, postPriorityEnabled bool) (*model.ThreadResponse, error) { if !threadMembership.Following { return nil, nil // in case the thread is not followed anymore - return nil error to be interpreted as 404 } @@ -462,6 +527,17 @@ func (s *SqlThreadStore) GetThreadForUser(threadMembership *model.ThreadMembersh LeftJoin("Posts ON Posts.Id = Threads.PostId"). Where(sq.Eq{"Threads.PostId": threadMembership.PostId}) + if postPriorityEnabled { + urgencyCase := sq. + Case(). + When(sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent}, "true"). + Else("false") + + query = query. + Column(sq.Alias(urgencyCase, "IsUrgent")). + LeftJoin("PostsPriority ON PostsPriority.PostId = Threads.PostId") + } + err := s.GetReplicaX().GetBuilder(&thread, query) if err != nil { if err == sql.ErrNoRows { diff --git a/store/store.go b/store/store.go index 055b0dfeea..c99b1fa7c2 100644 --- a/store/store.go +++ b/store/store.go @@ -84,6 +84,7 @@ type Store interface { SetContext(context context.Context) Context() context.Context NotifyAdmin() NotifyAdminStore + PostPriority() PostPriorityStore } type RetentionPolicyStore interface { @@ -240,9 +241,10 @@ type ChannelStore interface { PermanentDeleteMembersByUser(userID string) error PermanentDeleteMembersByChannel(channelID string) error UpdateLastViewedAt(channelIds []string, userID string) (map[string]int64, error) - UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) + UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error) - IncrementMentionCount(channelID string, userIDs []string, isRoot bool) error + CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) + IncrementMentionCount(channelID string, userIDs []string, isRoot, isUrgent bool) error AnalyticsTypeCount(teamID string, channelType model.ChannelType) (int64, error) GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) GetTeamMembersForChannel(channelID string) ([]string, error) @@ -322,9 +324,10 @@ type ThreadStore interface { GetTotalUnreadThreads(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error) GetTotalThreads(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error) GetTotalUnreadMentions(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error) + GetTotalUnreadUrgentMentions(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error) GetThreadsForUser(userId, teamID string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) - GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, error) - GetTeamsUnreadForUser(userID string, teamIDs []string) (map[string]*model.TeamUnread, error) + GetThreadForUser(threadMembership *model.ThreadMembership, extended, postPriorityIsEnabled bool) (*model.ThreadResponse, error) + GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) MarkAllAsRead(userID string, threadIds []string) error MarkAllAsReadByTeam(userID, teamID string) error @@ -970,6 +973,11 @@ type SharedChannelStore interface { UpdateAttachmentLastSyncAt(id string, syncTime int64) error } +type PostPriorityStore interface { + GetForPost(postId string) (*model.PostPriority, error) + GetForPosts(ids []string) ([]*model.PostPriority, error) +} + // ChannelSearchOpts contains options for searching channels. // // NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records. diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index c1843bc8be..28e66f1bef 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -104,6 +104,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("GetMembersForUserWithCursor", func(t *testing.T) { testChannelStoreGetMembersForUserWithCursor(t, ss) }) t.Run("GetMembersForUserWithPagination", func(t *testing.T) { testChannelStoreGetMembersForUserWithPagination(t, ss) }) t.Run("CountPostsAfter", func(t *testing.T) { testCountPostsAfter(t, ss) }) + t.Run("CountUrgentPostsAfter", func(t *testing.T) { testCountUrgentPostsAfter(t, ss) }) t.Run("UpdateLastViewedAt", func(t *testing.T) { testChannelStoreUpdateLastViewedAt(t, ss) }) t.Run("IncrementMentionCount", func(t *testing.T) { testChannelStoreIncrementMentionCount(t, ss) }) t.Run("UpdateChannelMember", func(t *testing.T) { testUpdateChannelMember(t, ss) }) @@ -4833,6 +4834,66 @@ func testCountPostsAfter(t *testing.T, ss store.Store) { }) } +func testCountUrgentPostsAfter(t *testing.T, ss store.Store) { + t.Run("should count all posts with or without the given user ID", func(t *testing.T) { + userId1 := model.NewId() + userId2 := model.NewId() + + channelId := model.NewId() + + p1, err := ss.Post().Save(&model.Post{ + UserId: userId1, + ChannelId: channelId, + CreateAt: 1000, + Metadata: &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString(model.PostPriorityUrgent), + RequestedAck: model.NewBool(false), + PersistentNotifications: model.NewBool(false), + }, + }, + }) + require.NoError(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: userId1, + ChannelId: channelId, + CreateAt: 1001, + Metadata: &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString("important"), + RequestedAck: model.NewBool(false), + PersistentNotifications: model.NewBool(false), + }, + }, + }) + require.NoError(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: userId2, + ChannelId: channelId, + CreateAt: 1002, + }) + require.NoError(t, err) + + count, err := ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt-1, "") + require.NoError(t, err) + assert.Equal(t, 1, count) + + count, err = ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt, "") + require.NoError(t, err) + assert.Equal(t, 0, count) + + count, err = ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt-1, userId1) + require.NoError(t, err) + assert.Equal(t, 1, count) + + count, err = ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt, userId1) + require.NoError(t, err) + assert.Equal(t, 0, count) + }) +} + func testChannelStoreUpdateLastViewedAt(t *testing.T, ss store.Store) { o1 := model.Channel{} o1.TeamId = model.NewId() @@ -4912,16 +4973,16 @@ func testChannelStoreIncrementMentionCount(t *testing.T, ss store.Store) { _, err := ss.Channel().SaveMember(&m1) require.NoError(t, err) - err = ss.Channel().IncrementMentionCount(m1.ChannelId, []string{m1.UserId}, false) + err = ss.Channel().IncrementMentionCount(m1.ChannelId, []string{m1.UserId}, false, false) require.NoError(t, err, "failed to update") - err = ss.Channel().IncrementMentionCount(m1.ChannelId, []string{"missing id"}, false) + err = ss.Channel().IncrementMentionCount(m1.ChannelId, []string{"missing id"}, false, false) require.NoError(t, err, "failed to update") - err = ss.Channel().IncrementMentionCount("missing id", []string{m1.UserId}, false) + err = ss.Channel().IncrementMentionCount("missing id", []string{m1.UserId}, false, false) require.NoError(t, err, "failed to update") - err = ss.Channel().IncrementMentionCount("missing id", []string{"missing id"}, false) + err = ss.Channel().IncrementMentionCount("missing id", []string{"missing id"}, false, false) require.NoError(t, err, "failed to update") } diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 0b73494d83..5cfe28d6a0 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -192,6 +192,27 @@ func (_m *ChannelStore) CountPostsAfter(channelID string, timestamp int64, userI return r0, r1, r2 } +// CountUrgentPostsAfter provides a mock function with given fields: channelID, timestamp, userID +func (_m *ChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) { + ret := _m.Called(channelID, timestamp, userID) + + var r0 int + if rf, ok := ret.Get(0).(func(string, int64, string) int); ok { + r0 = rf(channelID, timestamp, userID) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, int64, string) error); ok { + r1 = rf(channelID, timestamp, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateDirectChannel provides a mock function with given fields: userID, otherUserID, channelOptions func (_m *ChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) { _va := make([]interface{}, len(channelOptions)) @@ -1646,13 +1667,13 @@ func (_m *ChannelStore) GroupSyncedChannelCount() (int64, error) { return r0, r1 } -// IncrementMentionCount provides a mock function with given fields: channelID, userIDs, isRoot -func (_m *ChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool) error { - ret := _m.Called(channelID, userIDs, isRoot) +// IncrementMentionCount provides a mock function with given fields: channelID, userIDs, isRoot, isUrgent +func (_m *ChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error { + ret := _m.Called(channelID, userIDs, isRoot, isUrgent) var r0 error - if rf, ok := ret.Get(0).(func(string, []string, bool) error); ok { - r0 = rf(channelID, userIDs, isRoot) + if rf, ok := ret.Get(0).(func(string, []string, bool, bool) error); ok { + r0 = rf(channelID, userIDs, isRoot, isUrgent) } else { r0 = ret.Error(0) } @@ -2192,13 +2213,13 @@ func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userID string) ( return r0, r1 } -// UpdateLastViewedAtPost provides a mock function with given fields: unreadPost, userID, mentionCount, mentionCountRoot, setUnreadCountRoot -func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) { - ret := _m.Called(unreadPost, userID, mentionCount, mentionCountRoot, setUnreadCountRoot) +// UpdateLastViewedAtPost provides a mock function with given fields: unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot +func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) { + ret := _m.Called(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot) var r0 *model.ChannelUnreadAt - if rf, ok := ret.Get(0).(func(*model.Post, string, int, int, bool) *model.ChannelUnreadAt); ok { - r0 = rf(unreadPost, userID, mentionCount, mentionCountRoot, setUnreadCountRoot) + if rf, ok := ret.Get(0).(func(*model.Post, string, int, int, int, bool) *model.ChannelUnreadAt); ok { + r0 = rf(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.ChannelUnreadAt) @@ -2206,8 +2227,8 @@ func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID st } var r1 error - if rf, ok := ret.Get(1).(func(*model.Post, string, int, int, bool) error); ok { - r1 = rf(unreadPost, userID, mentionCount, mentionCountRoot, setUnreadCountRoot) + if rf, ok := ret.Get(1).(func(*model.Post, string, int, int, int, bool) error); ok { + r1 = rf(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot) } else { r1 = ret.Error(1) } diff --git a/store/storetest/mocks/PostPriorityStore.go b/store/storetest/mocks/PostPriorityStore.go new file mode 100644 index 0000000000..de126ec11a --- /dev/null +++ b/store/storetest/mocks/PostPriorityStore.go @@ -0,0 +1,61 @@ +// Code generated by mockery v2.10.4. DO NOT EDIT. + +// Regenerate this file using `make store-mocks`. + +package mocks + +import ( + model "github.com/mattermost/mattermost-server/v6/model" + mock "github.com/stretchr/testify/mock" +) + +// PostPriorityStore is an autogenerated mock type for the PostPriorityStore type +type PostPriorityStore struct { + mock.Mock +} + +// GetForPost provides a mock function with given fields: postId +func (_m *PostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) { + ret := _m.Called(postId) + + var r0 *model.PostPriority + if rf, ok := ret.Get(0).(func(string) *model.PostPriority); ok { + r0 = rf(postId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.PostPriority) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(postId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetForPosts provides a mock function with given fields: ids +func (_m *PostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) { + ret := _m.Called(ids) + + var r0 []*model.PostPriority + if rf, ok := ret.Get(0).(func([]string) []*model.PostPriority); ok { + r0 = rf(ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.PostPriority) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]string) error); ok { + r1 = rf(ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/store/storetest/mocks/Store.go b/store/storetest/mocks/Store.go index bd7c7e81c7..1d8e8ac326 100644 --- a/store/storetest/mocks/Store.go +++ b/store/storetest/mocks/Store.go @@ -475,6 +475,22 @@ func (_m *Store) Post() store.PostStore { return r0 } +// PostPriority provides a mock function with given fields: +func (_m *Store) PostPriority() store.PostPriorityStore { + ret := _m.Called() + + var r0 store.PostPriorityStore + if rf, ok := ret.Get(0).(func() store.PostPriorityStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.PostPriorityStore) + } + } + + return r0 +} + // Preference provides a mock function with given fields: func (_m *Store) Preference() store.PreferenceStore { ret := _m.Called() diff --git a/store/storetest/mocks/ThreadStore.go b/store/storetest/mocks/ThreadStore.go index 19d82db686..caf49a4e5e 100644 --- a/store/storetest/mocks/ThreadStore.go +++ b/store/storetest/mocks/ThreadStore.go @@ -119,13 +119,13 @@ func (_m *ThreadStore) GetMembershipsForUser(userId string, teamID string) ([]*m return r0, r1 } -// GetTeamsUnreadForUser provides a mock function with given fields: userID, teamIDs -func (_m *ThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) (map[string]*model.TeamUnread, error) { - ret := _m.Called(userID, teamIDs) +// GetTeamsUnreadForUser provides a mock function with given fields: userID, teamIDs, includeUrgentMentionCount +func (_m *ThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) { + ret := _m.Called(userID, teamIDs, includeUrgentMentionCount) var r0 map[string]*model.TeamUnread - if rf, ok := ret.Get(0).(func(string, []string) map[string]*model.TeamUnread); ok { - r0 = rf(userID, teamIDs) + if rf, ok := ret.Get(0).(func(string, []string, bool) map[string]*model.TeamUnread); ok { + r0 = rf(userID, teamIDs, includeUrgentMentionCount) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(map[string]*model.TeamUnread) @@ -133,8 +133,8 @@ func (_m *ThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) (m } var r1 error - if rf, ok := ret.Get(1).(func(string, []string) error); ok { - r1 = rf(userID, teamIDs) + if rf, ok := ret.Get(1).(func(string, []string, bool) error); ok { + r1 = rf(userID, teamIDs, includeUrgentMentionCount) } else { r1 = ret.Error(1) } @@ -165,13 +165,13 @@ func (_m *ThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive bool) return r0, r1 } -// GetThreadForUser provides a mock function with given fields: threadMembership, extended -func (_m *ThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, error) { - ret := _m.Called(threadMembership, extended) +// GetThreadForUser provides a mock function with given fields: threadMembership, extended, postPriorityIsEnabled +func (_m *ThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) { + ret := _m.Called(threadMembership, extended, postPriorityIsEnabled) var r0 *model.ThreadResponse - if rf, ok := ret.Get(0).(func(*model.ThreadMembership, bool) *model.ThreadResponse); ok { - r0 = rf(threadMembership, extended) + if rf, ok := ret.Get(0).(func(*model.ThreadMembership, bool, bool) *model.ThreadResponse); ok { + r0 = rf(threadMembership, extended, postPriorityIsEnabled) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.ThreadResponse) @@ -179,8 +179,8 @@ func (_m *ThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership } var r1 error - if rf, ok := ret.Get(1).(func(*model.ThreadMembership, bool) error); ok { - r1 = rf(threadMembership, extended) + if rf, ok := ret.Get(1).(func(*model.ThreadMembership, bool, bool) error); ok { + r1 = rf(threadMembership, extended, postPriorityIsEnabled) } else { r1 = ret.Error(1) } @@ -341,6 +341,27 @@ func (_m *ThreadStore) GetTotalUnreadThreads(userId string, teamID string, opts return r0, r1 } +// GetTotalUnreadUrgentMentions provides a mock function with given fields: userId, teamID, opts +func (_m *ThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) { + ret := _m.Called(userId, teamID, opts) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string, string, model.GetUserThreadsOpts) int64); ok { + r0 = rf(userId, teamID, opts) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, model.GetUserThreadsOpts) error); ok { + r1 = rf(userId, teamID, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // MaintainMembership provides a mock function with given fields: userID, postID, opts func (_m *ThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) { ret := _m.Called(userID, postID, opts) diff --git a/store/storetest/post_priority_store.go b/store/storetest/post_priority_store.go new file mode 100644 index 0000000000..68efa6ec8d --- /dev/null +++ b/store/storetest/post_priority_store.go @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package storetest + +import ( + "database/sql" + "errors" + "testing" + + "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPostPriorityStore(t *testing.T, ss store.Store, s SqlStore) { + t.Run("GetForPost", func(t *testing.T) { testPostPriorityStoreGetForPost(t, ss) }) +} + +func testPostPriorityStoreGetForPost(t *testing.T, ss store.Store) { + + t.Run("Save post priority when in post's metadata", func(t *testing.T) { + p1 := model.Post{} + p1.ChannelId = model.NewId() + p1.UserId = model.NewId() + p1.Message = NewTestId() + p1.Metadata = &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString("important"), + RequestedAck: model.NewBool(true), + PersistentNotifications: model.NewBool(false), + }, + } + + p2 := model.Post{} + p2.ChannelId = model.NewId() + p2.UserId = model.NewId() + p2.Message = NewTestId() + p2.Metadata = &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString(model.PostPriorityUrgent), + RequestedAck: model.NewBool(false), + PersistentNotifications: model.NewBool(true), + }, + } + + p3 := model.Post{} + p3.ChannelId = model.NewId() + p3.UserId = model.NewId() + p3.Message = NewTestId() + + _, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3}) + require.NoError(t, err) + require.Equal(t, -1, errIdx) + + pp1, err := ss.PostPriority().GetForPost(p1.Id) + require.NoError(t, err) + assert.Equal(t, "important", *pp1.Priority) + assert.Equal(t, true, *pp1.RequestedAck) + assert.Equal(t, false, *pp1.PersistentNotifications) + + pp2, err := ss.PostPriority().GetForPost(p2.Id) + require.NoError(t, err) + assert.Equal(t, model.PostPriorityUrgent, *pp2.Priority) + assert.Equal(t, false, *pp2.RequestedAck) + assert.Equal(t, true, *pp2.PersistentNotifications) + + _, err = ss.PostPriority().GetForPost(p3.Id) + assert.True(t, errors.Is(err, sql.ErrNoRows)) + }) +} diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index 677d049218..f3c7d91630 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -238,6 +238,31 @@ func testPostStoreSave(t *testing.T, ss store.Store) { assert.Greater(t, rchannel3.LastPostAt, rchannel2.LastPostAt) assert.Equal(t, int64(3), rchannel3.TotalMsgCount) }) + + t.Run("Save post with priority metadata set", func(t *testing.T) { + o1 := model.Post{} + o1.ChannelId = model.NewId() + o1.UserId = model.NewId() + o1.Message = NewTestId() + + o1.Metadata = &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString("important"), + RequestedAck: model.NewBool(true), + PersistentNotifications: model.NewBool(false), + }, + } + + p, err := ss.Post().Save(&o1) + require.NoError(t, err, "couldn't save item") + assert.Equal(t, int64(0), p.ReplyCount) + + pp, err := ss.PostPriority().GetForPost(p.Id) + require.NoError(t, err, "couldn't save item") + assert.Equal(t, "important", *pp.Priority) + assert.Equal(t, true, *pp.RequestedAck) + assert.Equal(t, false, *pp.PersistentNotifications) + }) } func testPostStoreSaveMultiple(t *testing.T, ss store.Store) { diff --git a/store/storetest/store.go b/store/storetest/store.go index 7979f8dac5..4f785c0426 100644 --- a/store/storetest/store.go +++ b/store/storetest/store.go @@ -56,6 +56,7 @@ type Store struct { ProductNoticesStore mocks.ProductNoticesStore context context.Context NotifyAdminStore mocks.NotifyAdminStore + PostPriorityStore mocks.PostPriorityStore } func (s *Store) SetContext(context context.Context) { s.context = context } @@ -100,6 +101,7 @@ func (s *Store) NotifyAdmin() store.NotifyAdminStore { return &s.NotifyAdmin func (s *Store) Group() store.GroupStore { return &s.GroupStore } func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore } func (s *Store) SharedChannel() store.SharedChannelStore { return &s.SharedChannelStore } +func (s *Store) PostPriority() store.PostPriorityStore { return &s.PostPriorityStore } func (s *Store) MarkSystemRanUnitTests() { /* do nothing */ } func (s *Store) Close() { /* do nothing */ } func (s *Store) LockToMaster() { /* do nothing */ } @@ -158,5 +160,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool { &s.ProductNoticesStore, &s.SharedChannelStore, &s.NotifyAdminStore, + &s.PostPriorityStore, ) } diff --git a/store/storetest/thread_store.go b/store/storetest/thread_store.go index e90e9f7d89..8bf646b503 100644 --- a/store/storetest/thread_store.go +++ b/store/storetest/thread_store.go @@ -32,7 +32,7 @@ func TestThreadStore(t *testing.T, ss store.Store, s SqlStore) { } func testThreadStorePopulation(t *testing.T, ss store.Store) { - makeSomePosts := func() []*model.Post { + makeSomePosts := func(urgent bool) []*model.Post { u1 := model.User{ Email: MakeEmail(), @@ -61,6 +61,16 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { o.UserId = u.Id o.Message = NewTestId() + if urgent { + o.Metadata = &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString(model.PostPriorityUrgent), + RequestedAck: model.NewBool(false), + PersistentNotifications: model.NewBool(false), + }, + } + } + otmp, err3 := ss.Post().Save(&o) require.NoError(t, err3) o2 := model.Post{} @@ -100,7 +110,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { return newPosts } t.Run("Save replies creates a thread", func(t *testing.T) { - newPosts := makeSomePosts() + newPosts := makeSomePosts(false) thread, err := ss.Thread().Get(newPosts[0].Id) require.NoError(t, err, "couldn't get thread") require.NotNil(t, thread) @@ -133,7 +143,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { }) t.Run("Delete a reply updates count on a thread", func(t *testing.T) { - newPosts := makeSomePosts() + newPosts := makeSomePosts(false) thread, err := ss.Thread().Get(newPosts[0].Id) require.NoError(t, err, "couldn't get thread") require.NotNil(t, thread) @@ -307,7 +317,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { }) t.Run("Thread membership 'viewed' timestamp is updated properly", func(t *testing.T) { - newPosts := makeSomePosts() + newPosts := makeSomePosts(false) opts := store.ThreadMembershipOpts{ Following: true, @@ -341,7 +351,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { }) t.Run("Thread membership 'viewed' timestamp is updated properly for new membership", func(t *testing.T) { - newPosts := makeSomePosts() + newPosts := makeSomePosts(false) opts := store.ThreadMembershipOpts{ Following: true, @@ -356,7 +366,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { }) t.Run("Updating post does not make thread unread", func(t *testing.T) { - newPosts := makeSomePosts() + newPosts := makeSomePosts(false) opts := store.ThreadMembershipOpts{ Following: true, IncrementMentions: false, @@ -366,14 +376,14 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { } m, err := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts) require.NoError(t, err) - th, err := ss.Thread().GetThreadForUser(m, false) + th, err := ss.Thread().GetThreadForUser(m, false, false) require.NoError(t, err) require.Equal(t, int64(2), th.UnreadReplies) m.LastViewed = newPosts[2].UpdateAt + 1 _, err = ss.Thread().UpdateMembership(m) require.NoError(t, err) - th, err = ss.Thread().GetThreadForUser(m, false) + th, err = ss.Thread().GetThreadForUser(m, false, false) require.NoError(t, err) require.Equal(t, int64(0), th.UnreadReplies) @@ -382,13 +392,13 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { _, err = ss.Post().Update(editedPost, newPosts[2]) require.NoError(t, err) - th, err = ss.Thread().GetThreadForUser(m, false) + th, err = ss.Thread().GetThreadForUser(m, false, false) require.NoError(t, err) require.Equal(t, int64(0), th.UnreadReplies) }) t.Run("Empty participantID should not appear in thread response", func(t *testing.T) { - newPosts := makeSomePosts() + newPosts := makeSomePosts(false) opts := store.ThreadMembershipOpts{ Following: true, IncrementMentions: false, @@ -399,7 +409,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { m, err := ss.Thread().MaintainMembership("", newPosts[0].Id, opts) require.NoError(t, err) m.UserId = newPosts[0].UserId - th, err := ss.Thread().GetThreadForUser(m, true) + th, err := ss.Thread().GetThreadForUser(m, true, false) require.NoError(t, err) for _, user := range th.Participants { require.NotNil(t, user) @@ -407,7 +417,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { }) t.Run("Get unread reply counts for thread", func(t *testing.T) { t.Skip("MM-41797") - newPosts := makeSomePosts() + newPosts := makeSomePosts(false) opts := store.ThreadMembershipOpts{ Following: true, IncrementMentions: false, @@ -435,6 +445,36 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { require.NoError(t, err) require.Equal(t, int64(2), unreads) }) + + testCases := []bool{true, false} + + for _, isUrgent := range testCases { + t.Run("Return is urgent for user thread/s", func(t *testing.T) { + newPosts := makeSomePosts(isUrgent) + opts := store.ThreadMembershipOpts{ + Following: true, + IncrementMentions: false, + UpdateFollowing: true, + UpdateViewedTimestamp: true, + UpdateParticipants: false, + } + + userID := newPosts[0].UserId + _, e := ss.Thread().MaintainMembership(userID, newPosts[0].Id, opts) + require.NoError(t, e) + + m, e := ss.Thread().GetMembershipForUser(userID, newPosts[0].Id) + require.NoError(t, e) + + th, e := ss.Thread().GetThreadForUser(m, false, true) + require.NoError(t, e) + require.Equal(t, isUrgent, th.IsUrgent) + + threads, e := ss.Thread().GetThreadsForUser(userID, "", model.GetUserThreadsOpts{IncludeIsUrgent: true}) + require.NoError(t, e) + require.Equal(t, isUrgent, threads[0].IsUrgent) + }) + } } func threadStoreCreateReply(t *testing.T, ss store.Store, channelID, postID, userID string, createAt int64) *model.Post { @@ -660,7 +700,7 @@ func testGetTeamsUnreadForUser(t *testing.T, ss store.Store) { threadStoreCreateReply(t, ss, channel1.Id, post.Id, post.UserId, model.GetMillis()) createThreadMembership(userID, post.Id) - teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}) + teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, true) require.NoError(t, err) assert.Len(t, teamsUnread, 1) assert.Equal(t, int64(1), teamsUnread[team1.Id].ThreadCount) @@ -674,7 +714,7 @@ func testGetTeamsUnreadForUser(t *testing.T, ss store.Store) { threadStoreCreateReply(t, ss, channel1.Id, post.Id, post.UserId, model.GetMillis()) createThreadMembership(userID, post.Id) - teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}) + teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, true) require.NoError(t, err) assert.Len(t, teamsUnread, 1) assert.Equal(t, int64(2), teamsUnread[team1.Id].ThreadCount) @@ -693,16 +733,24 @@ func testGetTeamsUnreadForUser(t *testing.T, ss store.Store) { Type: model.ChannelTypeOpen, }, -1) require.NoError(t, err) + post2, err := ss.Post().Save(&model.Post{ ChannelId: channel2.Id, UserId: userID, Message: model.NewRandomString(10), + Metadata: &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString(model.PostPriorityUrgent), + RequestedAck: model.NewBool(false), + PersistentNotifications: model.NewBool(false), + }, + }, }) require.NoError(t, err) threadStoreCreateReply(t, ss, channel2.Id, post2.Id, post2.UserId, model.GetMillis()) createThreadMembership(userID, post2.Id) - teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id, team2.Id}) + teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id, team2.Id}, true) require.NoError(t, err) assert.Len(t, teamsUnread, 2) assert.Equal(t, int64(2), teamsUnread[team1.Id].ThreadCount) @@ -715,11 +763,12 @@ func testGetTeamsUnreadForUser(t *testing.T, ss store.Store) { _, err = ss.Thread().MaintainMembership(userID, post2.Id, opts) require.NoError(t, err) - teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team2.Id}) + teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team2.Id}, true) require.NoError(t, err) assert.Len(t, teamsUnread, 1) assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadCount) assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadMentionCount) + assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadUrgentMentionCount) } type byPostId []*model.Post @@ -831,6 +880,13 @@ func testVarious(t *testing.T, ss store.Store) { ChannelId: team1channel1.Id, UserId: user1ID, Message: model.NewRandomString(10), + Metadata: &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString(model.PostPriorityUrgent), + RequestedAck: model.NewBool(false), + PersistentNotifications: model.NewBool(false), + }, + }, }) require.NoError(t, err) @@ -1032,6 +1088,33 @@ func testVarious(t *testing.T, ss store.Store) { } }) + t.Run("GetTotalUnreadUrgentMentions", func(t *testing.T) { + testCases := []struct { + Description string + UserID string + TeamID string + Options model.GetUserThreadsOpts + ExpectedThreads []*model.Post + }{ + {"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{ + team1channel1post3, + }}, + {"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{ + team1channel1post3, + }}, + {"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{}}, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + totalUnreadUrgentMentions, err := ss.Thread().GetTotalUnreadUrgentMentions(testCase.UserID, testCase.TeamID, testCase.Options) + require.NoError(t, err) + + assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalUnreadUrgentMentions) + }) + } + }) + assertThreadPosts := func(t *testing.T, threads []*model.ThreadResponse, expectedPosts []*model.Post) { t.Helper() @@ -1166,7 +1249,7 @@ func testMarkAllAsReadByChannels(t *testing.T, ss store.Store) { assertThreadReplyCount := func(t *testing.T, userID string, count int64) { t.Helper() - teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}) + teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, false) require.NoError(t, err) require.Len(t, teamsUnread, 1, "unexpected unread teams count") assert.Equal(t, count, teamsUnread[team1.Id].ThreadCount, "unexpected thread count") @@ -1623,7 +1706,7 @@ func testMarkAllAsReadByTeam(t *testing.T, ss store.Store) { assertThreadReplyCount := func(t *testing.T, userID, teamID string, count int64, message string) { t.Helper() - teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{teamID}) + teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{teamID}, true) require.NoError(t, err) require.Lenf(t, teamsUnread, 1, "unexpected unread teams count: %s", message) assert.Equalf(t, count, teamsUnread[teamID].ThreadCount, "unexpected thread count: %s", message) diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index c938056249..138c3cfb14 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -2468,7 +2468,7 @@ func testUserUnreadCount(t *testing.T, ss store.Store) { // Post one message with mention to open channel _, nErr = ss.Post().Save(&p1) require.NoError(t, nErr) - nErr = ss.Channel().IncrementMentionCount(c1.Id, []string{u2.Id, u3.Id}, false) + nErr = ss.Channel().IncrementMentionCount(c1.Id, []string{u2.Id, u3.Id}, false, false) require.NoError(t, nErr) // Post 2 messages without mention to direct channel @@ -2479,7 +2479,7 @@ func testUserUnreadCount(t *testing.T, ss store.Store) { _, nErr = ss.Post().Save(&p2) require.NoError(t, nErr) - nErr = ss.Channel().IncrementMentionCount(c2.Id, []string{u2.Id}, false) + nErr = ss.Channel().IncrementMentionCount(c2.Id, []string{u2.Id}, false, false) require.NoError(t, nErr) p3 := model.Post{} @@ -2489,7 +2489,7 @@ func testUserUnreadCount(t *testing.T, ss store.Store) { _, nErr = ss.Post().Save(&p3) require.NoError(t, nErr) - nErr = ss.Channel().IncrementMentionCount(c2.Id, []string{u2.Id}, false) + nErr = ss.Channel().IncrementMentionCount(c2.Id, []string{u2.Id}, false, false) require.NoError(t, nErr) badge, unreadCountErr := ss.User().GetUnreadCount(u2.Id, false) @@ -2501,7 +2501,7 @@ func testUserUnreadCount(t *testing.T, ss store.Store) { require.Equal(t, int64(1), badge, "should have 1 unread message") // Increment root mentions by 1 - nErr = ss.Channel().IncrementMentionCount(c1.Id, []string{u3.Id}, true) + nErr = ss.Channel().IncrementMentionCount(c1.Id, []string{u3.Id}, true, false) require.NoError(t, nErr) // CRT is enabled, only root mentions are counted diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 9e8bb2fade..ba5d2436e7 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -36,6 +36,7 @@ type TimerLayer struct { OAuthStore store.OAuthStore PluginStore store.PluginStore PostStore store.PostStore + PostPriorityStore store.PostPriorityStore PreferenceStore store.PreferenceStore ProductNoticesStore store.ProductNoticesStore ReactionStore store.ReactionStore @@ -130,6 +131,10 @@ func (s *TimerLayer) Post() store.PostStore { return s.PostStore } +func (s *TimerLayer) PostPriority() store.PostPriorityStore { + return s.PostPriorityStore +} + func (s *TimerLayer) Preference() store.PreferenceStore { return s.PreferenceStore } @@ -300,6 +305,11 @@ type TimerLayerPostStore struct { Root *TimerLayer } +type TimerLayerPostPriorityStore struct { + store.PostPriorityStore + Root *TimerLayer +} + type TimerLayerPreferenceStore struct { store.PreferenceStore Root *TimerLayer @@ -671,6 +681,22 @@ func (s *TimerLayerChannelStore) CountPostsAfter(channelID string, timestamp int return result, resultVar1, err } +func (s *TimerLayerChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) { + start := time.Now() + + result, err := s.ChannelStore.CountUrgentPostsAfter(channelID, timestamp, userID) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.CountUrgentPostsAfter", success, elapsed) + } + return result, err +} + func (s *TimerLayerChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) { start := time.Now() @@ -1711,10 +1737,10 @@ func (s *TimerLayerChannelStore) GroupSyncedChannelCount() (int64, error) { return result, err } -func (s *TimerLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool) error { +func (s *TimerLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error { start := time.Now() - err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot) + err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot, isUrgent) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -2248,10 +2274,10 @@ func (s *TimerLayerChannelStore) UpdateLastViewedAt(channelIds []string, userID return result, err } -func (s *TimerLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) { +func (s *TimerLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) { start := time.Now() - result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, setUnreadCountRoot) + result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -5891,6 +5917,38 @@ func (s *TimerLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) ( return result, err } +func (s *TimerLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) { + start := time.Now() + + result, err := s.PostPriorityStore.GetForPost(postId) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostPriorityStore.GetForPost", success, elapsed) + } + return result, err +} + +func (s *TimerLayerPostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) { + start := time.Now() + + result, err := s.PostPriorityStore.GetForPosts(ids) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostPriorityStore.GetForPosts", success, elapsed) + } + return result, err +} + func (s *TimerLayerPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) { start := time.Now() @@ -8881,10 +8939,10 @@ func (s *TimerLayerThreadStore) GetMembershipsForUser(userId string, teamID stri return result, err } -func (s *TimerLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) (map[string]*model.TeamUnread, error) { +func (s *TimerLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) { start := time.Now() - result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs) + result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs, includeUrgentMentionCount) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -8913,10 +8971,10 @@ func (s *TimerLayerThreadStore) GetThreadFollowers(threadID string, fetchOnlyAct return result, err } -func (s *TimerLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, error) { +func (s *TimerLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) { start := time.Now() - result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended) + result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended, postPriorityIsEnabled) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -9041,6 +9099,22 @@ func (s *TimerLayerThreadStore) GetTotalUnreadThreads(userId string, teamID stri return result, err } +func (s *TimerLayerThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) { + start := time.Now() + + result, err := s.ThreadStore.GetTotalUnreadUrgentMentions(userId, teamID, opts) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTotalUnreadUrgentMentions", success, elapsed) + } + return result, err +} + func (s *TimerLayerThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) { start := time.Now() @@ -11270,6 +11344,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay newStore.OAuthStore = &TimerLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore} newStore.PluginStore = &TimerLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore} newStore.PostStore = &TimerLayerPostStore{PostStore: childStore.Post(), Root: &newStore} + newStore.PostPriorityStore = &TimerLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore} newStore.PreferenceStore = &TimerLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore} newStore.ProductNoticesStore = &TimerLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore} newStore.ReactionStore = &TimerLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}