From fe352ab57f9c7a208914c9079ceba82be281df89 Mon Sep 17 00:00:00 2001 From: Eli Yukelzon Date: Fri, 30 Oct 2020 17:00:21 +0200 Subject: [PATCH] MM-29703 Mark threads as read when channels are marked (#15994) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jesús Espino --- app/channel.go | 6 +- app/channel_test.go | 2 +- app/notification.go | 2 +- einterfaces/mocks/LdapInterface.go | 10 +- einterfaces/mocks/OauthProvider.go | 11 +- model/thread.go | 1 + store/opentracinglayer/opentracinglayer.go | 48 ++++++- store/retrylayer/retrylayer.go | 52 ++++++- store/sqlstore/channel_store.go | 51 ++++++- store/sqlstore/post_store.go | 1 + store/sqlstore/thread_store.go | 50 ++++++- store/sqlstore/upgrade.go | 10 ++ store/store.go | 8 +- store/storetest/channel_store.go | 14 +- store/storetest/mocks/ChannelStore.go | 38 +++--- store/storetest/mocks/ThreadStore.go | 37 +++++ store/storetest/thread_store.go | 150 ++++++++++++++++++--- store/storetest/user_store.go | 6 +- store/timerlayer/timerlayer.go | 44 +++++- web/context_test.go | 2 + 20 files changed, 451 insertions(+), 92 deletions(-) diff --git a/app/channel.go b/app/channel.go index 11d6848819..e8526a3550 100644 --- a/app/channel.go +++ b/app/channel.go @@ -2322,7 +2322,7 @@ func (a *App) SetActiveChannel(userId string, channelId string) *model.AppError } func (a *App) UpdateChannelLastViewedAt(channelIds []string, userId string) *model.AppError { - if _, err := a.Srv().Store.Channel().UpdateLastViewedAt(channelIds, userId); err != nil { + if _, err := a.Srv().Store.Channel().UpdateLastViewedAt(channelIds, userId, *a.Config().ServiceSettings.ThreadAutoFollow); err != nil { var invErr *store.ErrInvalidInput switch { case errors.As(err, &invErr): @@ -2360,7 +2360,7 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string) (*model. return nil, err } - channelUnread, nErr := a.Srv().Store.Channel().UpdateLastViewedAtPost(post, userID, unreadMentions) + channelUnread, nErr := a.Srv().Store.Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, *a.Config().ServiceSettings.ThreadAutoFollow) if nErr != nil { return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError) } @@ -2531,7 +2531,7 @@ func (a *App) MarkChannelsAsViewed(channelIds []string, userId string, currentSe } } } - times, err := a.Srv().Store.Channel().UpdateLastViewedAt(channelIds, userId) + times, err := a.Srv().Store.Channel().UpdateLastViewedAt(channelIds, userId, *a.Config().ServiceSettings.ThreadAutoFollow) if err != nil { var invErr *store.ErrInvalidInput switch { diff --git a/app/channel_test.go b/app/channel_test.go index 5013bd255a..0ebf6a8e0f 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -1869,7 +1869,7 @@ func TestMarkChannelsAsViewedPanic(t *testing.T) { times := map[string]int64{ "userID": 1, } - mockChannelStore.On("UpdateLastViewedAt", []string{"channelID"}, "userID").Return(times, nil) + mockChannelStore.On("UpdateLastViewedAt", []string{"channelID"}, "userID", true).Return(times, nil) mockStore.On("User").Return(&mockUserStore) mockStore.On("Channel").Return(&mockChannelStore) diff --git a/app/notification.go b/app/notification.go index 52de649778..929685588c 100644 --- a/app/notification.go +++ b/app/notification.go @@ -184,7 +184,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod umc := make(chan *model.AppError, 1) go func(userId string) { defer close(umc) - nErr := a.Srv().Store.Channel().IncrementMentionCount(post.ChannelId, userId) + nErr := a.Srv().Store.Channel().IncrementMentionCount(post.ChannelId, userId, *a.Config().ServiceSettings.ThreadAutoFollow) if nErr != nil { umc <- model.NewAppError("SendNotifications", "app.channel.increment_mention_count.app_error", nil, nErr.Error(), http.StatusInternalServerError) return diff --git a/einterfaces/mocks/LdapInterface.go b/einterfaces/mocks/LdapInterface.go index 6c68d3431a..4389ce3776 100644 --- a/einterfaces/mocks/LdapInterface.go +++ b/einterfaces/mocks/LdapInterface.go @@ -71,13 +71,13 @@ func (_m *LdapInterface) DoLogin(id string, password string) (*model.User, *mode return r0, r1 } -// FirstLoginSync provides a mock function with given fields: userID, userAuthService, userAuthData, email -func (_m *LdapInterface) FirstLoginSync(userID string, userAuthService string, userAuthData string, email string) *model.AppError { - ret := _m.Called(userID, userAuthService, userAuthData, email) +// FirstLoginSync provides a mock function with given fields: user, userAuthService, userAuthData, email +func (_m *LdapInterface) FirstLoginSync(user *model.User, userAuthService string, userAuthData string, email string) *model.AppError { + ret := _m.Called(user, userAuthService, userAuthData, email) var r0 *model.AppError - if rf, ok := ret.Get(0).(func(string, string, string, string) *model.AppError); ok { - r0 = rf(userID, userAuthService, userAuthData, email) + if rf, ok := ret.Get(0).(func(*model.User, string, string, string) *model.AppError); ok { + r0 = rf(user, userAuthService, userAuthData, email) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.AppError) diff --git a/einterfaces/mocks/OauthProvider.go b/einterfaces/mocks/OauthProvider.go index 79ac3dfe2c..d6e71fa1e6 100644 --- a/einterfaces/mocks/OauthProvider.go +++ b/einterfaces/mocks/OauthProvider.go @@ -17,7 +17,7 @@ type OauthProvider struct { } // GetUserFromJson provides a mock function with given fields: data -func (_m *OauthProvider) GetUserFromJson(data io.Reader) *model.User { +func (_m *OauthProvider) GetUserFromJson(data io.Reader) (*model.User, error) { ret := _m.Called(data) var r0 *model.User @@ -29,5 +29,12 @@ func (_m *OauthProvider) GetUserFromJson(data io.Reader) *model.User { } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(io.Reader) error); ok { + r1 = rf(data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } diff --git a/model/thread.go b/model/thread.go index 8485032b2c..969a599e1e 100644 --- a/model/thread.go +++ b/model/thread.go @@ -9,6 +9,7 @@ import ( type Thread struct { PostId string `json:"id"` + ChannelId string `json:"channel_id"` ReplyCount int64 `json:"reply_count"` LastReplyAt int64 `json:"last_reply_at"` Participants StringArray `json:"participants"` diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index d11385192b..6bf69bbfcf 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -1538,7 +1538,7 @@ func (s *OpenTracingLayerChannelStore) GroupSyncedChannelCount() (int64, error) return result, err } -func (s *OpenTracingLayerChannelStore) IncrementMentionCount(channelId string, userId string) error { +func (s *OpenTracingLayerChannelStore) IncrementMentionCount(channelId string, userId string, updateThreads bool) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.IncrementMentionCount") s.Root.Store.SetContext(newCtx) @@ -1547,7 +1547,7 @@ func (s *OpenTracingLayerChannelStore) IncrementMentionCount(channelId string, u }() defer span.Finish() - err := s.ChannelStore.IncrementMentionCount(channelId, userId) + err := s.ChannelStore.IncrementMentionCount(channelId, userId, updateThreads) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -2074,7 +2074,7 @@ func (s *OpenTracingLayerChannelStore) Update(channel *model.Channel) (*model.Ch return result, err } -func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) { +func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateLastViewedAt") s.Root.Store.SetContext(newCtx) @@ -2083,7 +2083,7 @@ func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, u }() defer span.Finish() - result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId) + result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId, updateThreads) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -2092,7 +2092,7 @@ func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, u return result, err } -func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, error) { +func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateLastViewedAtPost") s.Root.Store.SetContext(newCtx) @@ -2101,7 +2101,7 @@ func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model. }() defer span.Finish() - result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount) + result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, updateThreads) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -7612,6 +7612,24 @@ func (s *OpenTracingLayerTermsOfServiceStore) Save(termsOfService *model.TermsOf return result, err } +func (s *OpenTracingLayerThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.CollectThreadsWithNewerReplies") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ThreadStore.CollectThreadsWithNewerReplies(userId, channelIds, timestamp) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.CreateMembershipIfNeeded") @@ -7810,6 +7828,24 @@ func (s *OpenTracingLayerThreadStore) UpdateMembership(membership *model.ThreadM return result, err } +func (s *OpenTracingLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.UpdateUnreadsByChannel") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + func (s *OpenTracingLayerTokenStore) Cleanup() { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.Cleanup") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 58b49a9020..b20b524c92 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -1670,11 +1670,11 @@ func (s *RetryLayerChannelStore) GroupSyncedChannelCount() (int64, error) { } -func (s *RetryLayerChannelStore) IncrementMentionCount(channelId string, userId string) error { +func (s *RetryLayerChannelStore) IncrementMentionCount(channelId string, userId string, updateThreads bool) error { tries := 0 for { - err := s.ChannelStore.IncrementMentionCount(channelId, userId) + err := s.ChannelStore.IncrementMentionCount(channelId, userId, updateThreads) if err == nil { return nil } @@ -2198,11 +2198,11 @@ func (s *RetryLayerChannelStore) Update(channel *model.Channel) (*model.Channel, } -func (s *RetryLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) { +func (s *RetryLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error) { tries := 0 for { - result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId) + result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId, updateThreads) if err == nil { return result, nil } @@ -2218,11 +2218,11 @@ func (s *RetryLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId } -func (s *RetryLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, error) { +func (s *RetryLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error) { tries := 0 for { - result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount) + result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, updateThreads) if err == nil { return result, nil } @@ -7642,6 +7642,26 @@ func (s *RetryLayerTermsOfServiceStore) Save(termsOfService *model.TermsOfServic } +func (s *RetryLayerThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) { + + tries := 0 + for { + result, err := s.ThreadStore.CollectThreadsWithNewerReplies(userId, channelIds, timestamp) + 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 + } + } + +} + func (s *RetryLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string) error { tries := 0 @@ -7862,6 +7882,26 @@ func (s *RetryLayerThreadStore) UpdateMembership(membership *model.ThreadMembers } +func (s *RetryLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error { + + tries := 0 + for { + err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + } + +} + func (s *RetryLayerTokenStore) Cleanup() { s.TokenStore.Cleanup() diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 6e0b9e4027..06fd2436e7 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -2046,7 +2046,17 @@ func (s SqlChannelStore) PermanentDeleteMembersByUser(userId string) error { return nil } -func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) { +func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error) { + var threadsToUpdate []string + now := model.GetMillis() + if updateThreads { + var err error + threadsToUpdate, err = s.Thread().CollectThreadsWithNewerReplies(userId, channelIds, now) + if err != nil { + return nil, err + } + } + keys, props := MapStringsToQueryParams(channelIds, "Channel") props["UserId"] = userId @@ -2089,11 +2099,15 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) for _, t := range lastPostAtTimes { times[t.Id] = t.LastPostAt } + if updateThreads { + s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now) + } return times, nil } msgCountQuery := "" lastViewedQuery := "" + for index, t := range lastPostAtTimes { times[t.Id] = t.LastPostAt @@ -2121,6 +2135,9 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) return nil, errors.Wrapf(err, "failed to update ChannelMembers with userId=%s and channelId in %v", userId, channelIds) } + if updateThreads { + s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now) + } return times, nil } @@ -2168,8 +2185,16 @@ func (s SqlChannelStore) CountPostsAfter(channelId string, timestamp int64, user // 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 int) (*model.ChannelUnreadAt, error) { +func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error) { + var threadsToUpdate []string unreadDate := unreadPost.CreateAt - 1 + if updateThreads { + var err error + threadsToUpdate, err = s.Thread().CollectThreadsWithNewerReplies(userID, []string{unreadPost.ChannelId}, unreadDate) + if err != nil { + return nil, err + } + } unread, err := s.CountPostsAfter(unreadPost.ChannelId, unreadDate, "") if err != nil { @@ -2225,10 +2250,24 @@ func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID s if err = s.GetMaster().SelectOne(result, chanUnreadQuery, params); err != nil { return nil, errors.Wrapf(err, "failed to get ChannelMember with channelId=%s", unreadPost.ChannelId) } + + if updateThreads { + s.Thread().UpdateUnreadsByChannel(userID, threadsToUpdate, unreadDate) + } return result, nil } -func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string) error { +func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string, updateThreads bool) error { + now := model.GetMillis() + var threadsToUpdate []string + if updateThreads { + var err error + threadsToUpdate, err = s.Thread().CollectThreadsWithNewerReplies(userId, []string{channelId}, now) + if err != nil { + return err + } + } + _, err := s.GetMaster().Exec( `UPDATE ChannelMembers @@ -2238,11 +2277,13 @@ func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string) WHERE UserId = :UserId AND ChannelId = :ChannelId`, - map[string]interface{}{"ChannelId": channelId, "UserId": userId, "LastUpdateAt": model.GetMillis()}) + map[string]interface{}{"ChannelId": channelId, "UserId": userId, "LastUpdateAt": now}) if err != nil { return errors.Wrapf(err, "failed to Update ChannelMembers with channelId=%s and userId=%s", channelId, userId) } - + if updateThreads { + s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now) + } return nil } diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index 865734bb32..7df9f8f4be 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -1987,6 +1987,7 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *gorp.Transaction, pos // no metadata entry, create one if err := transaction.Insert(&model.Thread{ PostId: rootId, + ChannelId: posts[0].ChannelId, ReplyCount: count, LastReplyAt: now, Participants: participants, diff --git a/store/sqlstore/thread_store.go b/store/sqlstore/thread_store.go index 92ffaff7ef..58a5e319d7 100644 --- a/store/sqlstore/thread_store.go +++ b/store/sqlstore/thread_store.go @@ -5,11 +5,12 @@ package sqlstore import ( "database/sql" + "time" + "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/store" "github.com/mattermost/mattermost-server/v5/utils" "github.com/pkg/errors" - "time" sq "github.com/Masterminds/squirrel" ) @@ -29,6 +30,7 @@ func newSqlThreadStore(sqlStore SqlStore) store.ThreadStore { for _, db := range sqlStore.GetAllConns() { tableThreads := db.AddTableWithName(model.Thread{}, "Threads").SetKeys(false, "PostId") tableThreads.ColMap("PostId").SetMaxSize(26) + tableThreads.ColMap("ChannelId").SetMaxSize(26) tableThreads.ColMap("Participants").SetMaxSize(0) tableThreadMemberships := db.AddTableWithName(model.ThreadMembership{}, "ThreadMemberships").SetKeys(false, "PostId", "UserId") tableThreadMemberships.ColMap("PostId").SetMaxSize(26) @@ -39,12 +41,13 @@ func newSqlThreadStore(sqlStore SqlStore) store.ThreadStore { } func threadSliceColumns() []string { - return []string{"PostId", "LastReplyAt", "ReplyCount", "Participants"} + return []string{"PostId", "ChannelId", "LastReplyAt", "ReplyCount", "Participants"} } func threadToSlice(thread *model.Thread) []interface{} { return []interface{}{ thread.PostId, + thread.ChannelId, thread.LastReplyAt, thread.ReplyCount, thread.Participants, @@ -52,13 +55,10 @@ func threadToSlice(thread *model.Thread) []interface{} { } func (s *SqlThreadStore) createIndexesIfNotExists() { - s.CreateIndexIfNotExists("idx_threads_last_reply_at", "Threads", "LastReplyAt") - s.CreateIndexIfNotExists("idx_threads_post_id", "Threads", "PostId") - s.CreateIndexIfNotExists("idx_thread_memberships_last_update_at", "ThreadMemberships", "LastUpdated") s.CreateIndexIfNotExists("idx_thread_memberships_last_view_at", "ThreadMemberships", "LastViewed") - s.CreateIndexIfNotExists("idx_thread_memberships_post_id", "ThreadMemberships", "PostId") s.CreateIndexIfNotExists("idx_thread_memberships_user_id", "ThreadMemberships", "UserId") + s.CreateIndexIfNotExists("idx_threads_channel_id", "Threads", "ChannelId") } func (s *SqlThreadStore) SaveMultiple(threads []*model.Thread) ([]*model.Thread, int, error) { @@ -188,3 +188,41 @@ func (s *SqlThreadStore) CreateMembershipIfNeeded(userId, postId string) error { }) return err } + +func (s *SqlThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) { + var changedThreads []string + query, args, _ := s.getQueryBuilder(). + Select("Threads.PostId"). + From("Threads"). + LeftJoin("ChannelMembers ON ChannelMembers.ChannelId=Threads.ChannelId"). + Where(sq.And{ + sq.Eq{"Threads.ChannelId": channelIds}, + sq.Eq{"ChannelMembers.UserId": userId}, + sq.Or{ + sq.Expr("Threads.LastReplyAt >= ChannelMembers.LastViewedAt"), + sq.GtOrEq{"Threads.LastReplyAt": timestamp}, + }, + }). + ToSql() + if _, err := s.GetReplica().Select(&changedThreads, query, args...); err != nil { + return nil, errors.Wrap(err, "failed to fetch threads") + } + return changedThreads, nil +} + +func (s *SqlThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error { + if len(changedThreads) == 0 { + return nil + } + updateQuery, updateArgs, _ := s.getQueryBuilder(). + Update("ThreadMemberships"). + Where(sq.Eq{"UserId": userId, "PostId": changedThreads}). + Set("LastUpdated", timestamp). + Set("LastViewed", timestamp). + ToSql() + if _, err := s.GetMaster().Exec(updateQuery, updateArgs...); err != nil { + return errors.Wrap(err, "failed to update thread membership") + } + + return nil +} diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index d82eb2688a..40b4ac5c69 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -925,6 +925,16 @@ func upgradeDatabaseToVersion529(sqlStore SqlStore) { sqlStore.AlterColumnTypeIfExists("SidebarChannels", "CategoryId", "VARCHAR(128)", "VARCHAR(128)") sqlStore.AlterColumnDefaultIfExists("SidebarChannels", "CategoryId", model.NewString(""), nil) + sqlStore.CreateColumnIfNotExistsNoDefault("Threads", "ChannelId", "VARCHAR(26)", "VARCHAR(26)") + + updateThreadChannelsQuery := "UPDATE Threads INNER JOIN Posts ON Posts.Id=Threads.PostId SET Threads.ChannelId=Posts.ChannelId WHERE Threads.ChannelId IS NULL" + if sqlStore.DriverName() == model.DATABASE_DRIVER_POSTGRES { + updateThreadChannelsQuery = "UPDATE Threads SET ChannelId=Posts.ChannelId FROM Posts WHERE Posts.Id=Threads.PostId AND Threads.ChannelId IS NULL" + } + if _, err := sqlStore.GetMaster().Exec(updateThreadChannelsQuery); err != nil { + mlog.Error("Error updating ChannelId in Threads table", mlog.Err(err)) + } + saveSchemaVersion(sqlStore, VERSION_5_29_0) } } diff --git a/store/store.go b/store/store.go index 0f1c946056..b9e2b4834b 100644 --- a/store/store.go +++ b/store/store.go @@ -191,10 +191,10 @@ type ChannelStore interface { RemoveMembers(channelId string, userIds []string) error PermanentDeleteMembersByUser(userId string) error PermanentDeleteMembersByChannel(channelId string) error - UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) - UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, error) + UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error) + UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error) CountPostsAfter(channelId string, timestamp int64, userId string) (int, error) - IncrementMentionCount(channelId string, userId string) error + IncrementMentionCount(channelId string, userId string, updateThreads bool) error AnalyticsTypeCount(teamId string, channelType string) (int64, error) GetMembersForUser(teamId string, userId string) (*model.ChannelMembers, error) GetMembersForUserWithPagination(teamId, userId string, page, perPage int) (*model.ChannelMembers, error) @@ -259,6 +259,8 @@ type ThreadStore interface { GetMembershipForUser(userId, postId string) (*model.ThreadMembership, error) DeleteMembershipForUser(userId, postId string) error CreateMembershipIfNeeded(userId, postId string) error + CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) + UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error } type PostStore interface { diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 55e6178ab9..473b84728c 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -4157,11 +4157,11 @@ func testChannelStoreUpdateLastViewedAt(t *testing.T, ss store.Store) { require.Nil(t, err) var times map[string]int64 - times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, m1.UserId) + times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, m1.UserId, false) require.Nil(t, err, "failed to update ", err) require.Equal(t, o1.LastPostAt, times[o1.Id], "last viewed at time incorrect") - times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId, m2.ChannelId}, m1.UserId) + times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId, m2.ChannelId}, m1.UserId, false) require.Nil(t, err, "failed to update ", err) require.Equal(t, o2.LastPostAt, times[o2.Id], "last viewed at time incorrect") @@ -4177,7 +4177,7 @@ func testChannelStoreUpdateLastViewedAt(t *testing.T, ss store.Store) { assert.Equal(t, o2.LastPostAt, rm2.LastUpdateAt) assert.Equal(t, o2.TotalMsgCount, rm2.MsgCount) - _, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, "missing id") + _, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, "missing id", false) require.Nil(t, err, "failed to update") } @@ -4198,16 +4198,16 @@ func testChannelStoreIncrementMentionCount(t *testing.T, ss store.Store) { _, err := ss.Channel().SaveMember(&m1) require.Nil(t, err) - err = ss.Channel().IncrementMentionCount(m1.ChannelId, m1.UserId) + err = ss.Channel().IncrementMentionCount(m1.ChannelId, m1.UserId, false) require.Nil(t, err, "failed to update") - err = ss.Channel().IncrementMentionCount(m1.ChannelId, "missing id") + err = ss.Channel().IncrementMentionCount(m1.ChannelId, "missing id", false) require.Nil(t, err, "failed to update") - err = ss.Channel().IncrementMentionCount("missing id", m1.UserId) + err = ss.Channel().IncrementMentionCount("missing id", m1.UserId, false) require.Nil(t, err, "failed to update") - err = ss.Channel().IncrementMentionCount("missing id", "missing id") + err = ss.Channel().IncrementMentionCount("missing id", "missing id", false) require.Nil(t, err, "failed to update") } diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 071abdc306..249c14a3eb 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -1252,13 +1252,13 @@ func (_m *ChannelStore) GroupSyncedChannelCount() (int64, error) { return r0, r1 } -// IncrementMentionCount provides a mock function with given fields: channelId, userId -func (_m *ChannelStore) IncrementMentionCount(channelId string, userId string) error { - ret := _m.Called(channelId, userId) +// IncrementMentionCount provides a mock function with given fields: channelId, userId, updateThreads +func (_m *ChannelStore) IncrementMentionCount(channelId string, userId string, updateThreads bool) error { + ret := _m.Called(channelId, userId, updateThreads) var r0 error - if rf, ok := ret.Get(0).(func(string, string) error); ok { - r0 = rf(channelId, userId) + if rf, ok := ret.Get(0).(func(string, string, bool) error); ok { + r0 = rf(channelId, userId, updateThreads) } else { r0 = ret.Error(0) } @@ -1752,13 +1752,13 @@ func (_m *ChannelStore) Update(channel *model.Channel) (*model.Channel, error) { return r0, r1 } -// UpdateLastViewedAt provides a mock function with given fields: channelIds, userId -func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) { - ret := _m.Called(channelIds, userId) +// UpdateLastViewedAt provides a mock function with given fields: channelIds, userId, updateThreads +func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error) { + ret := _m.Called(channelIds, userId, updateThreads) var r0 map[string]int64 - if rf, ok := ret.Get(0).(func([]string, string) map[string]int64); ok { - r0 = rf(channelIds, userId) + if rf, ok := ret.Get(0).(func([]string, string, bool) map[string]int64); ok { + r0 = rf(channelIds, userId, updateThreads) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(map[string]int64) @@ -1766,8 +1766,8 @@ func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userId string) ( } var r1 error - if rf, ok := ret.Get(1).(func([]string, string) error); ok { - r1 = rf(channelIds, userId) + if rf, ok := ret.Get(1).(func([]string, string, bool) error); ok { + r1 = rf(channelIds, userId, updateThreads) } else { r1 = ret.Error(1) } @@ -1775,13 +1775,13 @@ func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userId string) ( return r0, r1 } -// UpdateLastViewedAtPost provides a mock function with given fields: unreadPost, userID, mentionCount -func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, error) { - ret := _m.Called(unreadPost, userID, mentionCount) +// UpdateLastViewedAtPost provides a mock function with given fields: unreadPost, userID, mentionCount, updateThreads +func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error) { + ret := _m.Called(unreadPost, userID, mentionCount, updateThreads) var r0 *model.ChannelUnreadAt - if rf, ok := ret.Get(0).(func(*model.Post, string, int) *model.ChannelUnreadAt); ok { - r0 = rf(unreadPost, userID, mentionCount) + if rf, ok := ret.Get(0).(func(*model.Post, string, int, bool) *model.ChannelUnreadAt); ok { + r0 = rf(unreadPost, userID, mentionCount, updateThreads) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.ChannelUnreadAt) @@ -1789,8 +1789,8 @@ func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID st } var r1 error - if rf, ok := ret.Get(1).(func(*model.Post, string, int) error); ok { - r1 = rf(unreadPost, userID, mentionCount) + if rf, ok := ret.Get(1).(func(*model.Post, string, int, bool) error); ok { + r1 = rf(unreadPost, userID, mentionCount, updateThreads) } else { r1 = ret.Error(1) } diff --git a/store/storetest/mocks/ThreadStore.go b/store/storetest/mocks/ThreadStore.go index 24a367db4f..e8d762e152 100644 --- a/store/storetest/mocks/ThreadStore.go +++ b/store/storetest/mocks/ThreadStore.go @@ -14,6 +14,29 @@ type ThreadStore struct { mock.Mock } +// CollectThreadsWithNewerReplies provides a mock function with given fields: userId, channelIds, timestamp +func (_m *ThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) { + ret := _m.Called(userId, channelIds, timestamp) + + var r0 []string + if rf, ok := ret.Get(0).(func(string, []string, int64) []string); ok { + r0 = rf(userId, channelIds, timestamp) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, []string, int64) error); ok { + r1 = rf(userId, channelIds, timestamp) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateMembershipIfNeeded provides a mock function with given fields: userId, postId func (_m *ThreadStore) CreateMembershipIfNeeded(userId string, postId string) error { ret := _m.Called(userId, postId) @@ -246,3 +269,17 @@ func (_m *ThreadStore) UpdateMembership(membership *model.ThreadMembership) (*mo return r0, r1 } + +// UpdateUnreadsByChannel provides a mock function with given fields: userId, changedThreads, timestamp +func (_m *ThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error { + ret := _m.Called(userId, changedThreads, timestamp) + + var r0 error + if rf, ok := ret.Get(0).(func(string, []string, int64) error); ok { + r0 = rf(userId, changedThreads, timestamp) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/store/storetest/thread_store.go b/store/storetest/thread_store.go index 62edf5e2c1..deed3f3db4 100644 --- a/store/storetest/thread_store.go +++ b/store/storetest/thread_store.go @@ -6,8 +6,10 @@ package storetest import ( "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/store" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" + "time" ) func TestThreadStore(t *testing.T, ss store.Store, s SqlSupplier) { @@ -16,42 +18,72 @@ func TestThreadStore(t *testing.T, ss store.Store, s SqlSupplier) { func testThreadStorePopulation(t *testing.T, ss store.Store) { makeSomePosts := func() []*model.Post { - o1 := model.Post{} - o1.ChannelId = model.NewId() - o1.UserId = model.NewId() - o1.RootId = model.NewId() - o1.Message = "zz" + model.NewId() + "b" + u1 := model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + + u, err := ss.User().Save(&u1) + require.Nil(t, err) + + c, err2 := ss.Channel().Save(&model.Channel{ + DisplayName: model.NewId(), + Type: model.CHANNEL_OPEN, + Name: model.NewId(), + }, 999) + require.NoError(t, err2) + + _, err44 := ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c.Id, + UserId: u1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + MsgCount: 90, + }) + require.NoError(t, err44) + o := model.Post{} + o.ChannelId = c.Id + o.UserId = u.Id + o.Message = "zz" + model.NewId() + "b" + + otmp, err3 := ss.Post().Save(&o) + require.NoError(t, err3) o2 := model.Post{} - o2.ChannelId = model.NewId() + o2.ChannelId = c.Id o2.UserId = model.NewId() - o2.RootId = o1.RootId + o2.RootId = otmp.Id o2.Message = "zz" + model.NewId() + "b" o3 := model.Post{} - o3.ChannelId = model.NewId() - o3.UserId = model.NewId() - o3.RootId = model.NewId() + o3.ChannelId = c.Id + o3.UserId = u.Id + o3.RootId = otmp.Id o3.Message = "zz" + model.NewId() + "b" o4 := model.Post{} - o4.ChannelId = model.NewId() + o4.ChannelId = c.Id o4.UserId = model.NewId() o4.Message = "zz" + model.NewId() + "b" - newPosts, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&o1, &o2, &o3, &o4}) - require.Nil(t, err, "couldn't save item") + newPosts, errIdx, err3 := ss.Post().SaveMultiple([]*model.Post{&o2, &o3, &o4}) + + olist, _ := ss.Post().Get(otmp.Id, true) + o1 := olist.Posts[olist.Order[0]] + + newPosts = append([]*model.Post{o1}, newPosts...) + require.Nil(t, err3, "couldn't save item") require.Equal(t, -1, errIdx) require.Len(t, newPosts, 4) require.Equal(t, int64(2), newPosts[0].ReplyCount) require.Equal(t, int64(2), newPosts[1].ReplyCount) - require.Equal(t, int64(1), newPosts[2].ReplyCount) + require.Equal(t, int64(2), newPosts[2].ReplyCount) require.Equal(t, int64(0), newPosts[3].ReplyCount) + return newPosts } t.Run("Save replies creates a thread", func(t *testing.T) { newPosts := makeSomePosts() - thread, err := ss.Thread().Get(newPosts[0].RootId) + thread, err := ss.Thread().Get(newPosts[0].Id) require.Nil(t, err, "couldn't get thread") require.NotNil(t, thread) require.Equal(t, int64(2), thread.ReplyCount) @@ -60,13 +92,13 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { o5 := model.Post{} o5.ChannelId = model.NewId() o5.UserId = model.NewId() - o5.RootId = newPosts[0].RootId + o5.RootId = newPosts[0].Id o5.Message = "zz" + model.NewId() + "b" _, _, err = ss.Post().SaveMultiple([]*model.Post{&o5}) require.Nil(t, err, "couldn't save item") - thread, err = ss.Thread().Get(newPosts[0].RootId) + thread, err = ss.Thread().Get(newPosts[0].Id) require.Nil(t, err, "couldn't get thread") require.NotNil(t, thread) require.Equal(t, int64(3), thread.ReplyCount) @@ -75,7 +107,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() - thread, err := ss.Thread().Get(newPosts[0].RootId) + thread, err := ss.Thread().Get(newPosts[0].Id) require.Nil(t, err, "couldn't get thread") require.NotNil(t, thread) require.Equal(t, int64(2), thread.ReplyCount) @@ -84,7 +116,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { err = ss.Post().Delete(newPosts[1].Id, 1234, model.NewId()) require.Nil(t, err, "couldn't delete post") - thread, err = ss.Thread().Get(newPosts[0].RootId) + thread, err = ss.Thread().Get(newPosts[0].Id) require.Nil(t, err, "couldn't get thread") require.NotNil(t, thread) require.Equal(t, int64(1), thread.ReplyCount) @@ -197,4 +229,84 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { thread2, _ := ss.Thread().Get(rootPost.Id) require.Nil(t, thread2) }) + + t.Run("Thread last updated is changed when channel is updated after UpdateLastViewedAtPost", func(t *testing.T) { + newPosts := makeSomePosts() + + require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id)) + m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) + require.Nil(t, err1) + m.LastUpdated -= 1000 + _, err := ss.Thread().UpdateMembership(m) + require.Nil(t, err) + + _, err = ss.Channel().UpdateLastViewedAtPost(newPosts[0], newPosts[0].UserId, 0, true) + require.Nil(t, err) + + assert.Eventually(t, func() bool { + m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) + require.Nil(t, err2) + return m2.LastUpdated > m.LastUpdated + }, time.Second, 10*time.Millisecond) + }) + + t.Run("Thread last updated is changed when channel is updated after IncrementMentionCount", func(t *testing.T) { + newPosts := makeSomePosts() + + require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id)) + m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) + require.Nil(t, err1) + m.LastUpdated -= 1000 + _, err := ss.Thread().UpdateMembership(m) + require.Nil(t, err) + + err = ss.Channel().IncrementMentionCount(newPosts[0].ChannelId, newPosts[0].UserId, true) + require.Nil(t, err) + + assert.Eventually(t, func() bool { + m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) + require.Nil(t, err2) + return m2.LastUpdated > m.LastUpdated + }, time.Second, 10*time.Millisecond) + }) + + t.Run("Thread last updated is changed when channel is updated after UpdateLastViewedAt", func(t *testing.T) { + newPosts := makeSomePosts() + + require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id)) + m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) + require.Nil(t, err1) + m.LastUpdated -= 1000 + _, err := ss.Thread().UpdateMembership(m) + require.Nil(t, err) + + _, err = ss.Channel().UpdateLastViewedAt([]string{newPosts[0].ChannelId}, newPosts[0].UserId, true) + require.Nil(t, err) + + assert.Eventually(t, func() bool { + m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) + require.Nil(t, err2) + return m2.LastUpdated > m.LastUpdated + }, time.Second, 10*time.Millisecond) + }) + + t.Run("Thread last updated is changed when channel is updated after UpdateLastViewedAtPost for mark unread", func(t *testing.T) { + newPosts := makeSomePosts() + + require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id)) + m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) + require.Nil(t, err1) + m.LastUpdated += 1000 + _, err := ss.Thread().UpdateMembership(m) + require.Nil(t, err) + + _, err = ss.Channel().UpdateLastViewedAtPost(newPosts[0], newPosts[0].UserId, 0, true) + require.Nil(t, err) + + assert.Eventually(t, func() bool { + m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) + require.Nil(t, err2) + return m2.LastUpdated < m.LastUpdated + }, time.Second, 10*time.Millisecond) + }) } diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 1e91add4e8..ae1e69a906 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -2172,7 +2172,7 @@ func testUserUnreadCount(t *testing.T, ss store.Store) { // Post one message with mention to open channel _, nErr = ss.Post().Save(&p1) require.Nil(t, nErr) - nErr = ss.Channel().IncrementMentionCount(c1.Id, u2.Id) + nErr = ss.Channel().IncrementMentionCount(c1.Id, u2.Id, false) require.Nil(t, nErr) // Post 2 messages without mention to direct channel @@ -2183,7 +2183,7 @@ func testUserUnreadCount(t *testing.T, ss store.Store) { _, nErr = ss.Post().Save(&p2) require.Nil(t, nErr) - nErr = ss.Channel().IncrementMentionCount(c2.Id, u2.Id) + nErr = ss.Channel().IncrementMentionCount(c2.Id, u2.Id, false) require.Nil(t, nErr) p3 := model.Post{} @@ -2193,7 +2193,7 @@ func testUserUnreadCount(t *testing.T, ss store.Store) { _, nErr = ss.Post().Save(&p3) require.Nil(t, nErr) - nErr = ss.Channel().IncrementMentionCount(c2.Id, u2.Id) + nErr = ss.Channel().IncrementMentionCount(c2.Id, u2.Id, false) require.Nil(t, nErr) badge, unreadCountErr := ss.User().GetUnreadCount(u2.Id) diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index c1f3a62d61..2e65b7183e 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -1415,10 +1415,10 @@ func (s *TimerLayerChannelStore) GroupSyncedChannelCount() (int64, error) { return result, err } -func (s *TimerLayerChannelStore) IncrementMentionCount(channelId string, userId string) error { +func (s *TimerLayerChannelStore) IncrementMentionCount(channelId string, userId string, updateThreads bool) error { start := timemodule.Now() - err := s.ChannelStore.IncrementMentionCount(channelId, userId) + err := s.ChannelStore.IncrementMentionCount(channelId, userId, updateThreads) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) if s.Root.Metrics != nil { @@ -1920,10 +1920,10 @@ func (s *TimerLayerChannelStore) Update(channel *model.Channel) (*model.Channel, return result, err } -func (s *TimerLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) { +func (s *TimerLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error) { start := timemodule.Now() - result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId) + result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId, updateThreads) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) if s.Root.Metrics != nil { @@ -1936,10 +1936,10 @@ func (s *TimerLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId return result, err } -func (s *TimerLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, error) { +func (s *TimerLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error) { start := timemodule.Now() - result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount) + result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, updateThreads) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) if s.Root.Metrics != nil { @@ -6872,6 +6872,22 @@ func (s *TimerLayerTermsOfServiceStore) Save(termsOfService *model.TermsOfServic return result, err } +func (s *TimerLayerThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) { + start := timemodule.Now() + + result, err := s.ThreadStore.CollectThreadsWithNewerReplies(userId, channelIds, timestamp) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.CollectThreadsWithNewerReplies", success, elapsed) + } + return result, err +} + func (s *TimerLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string) error { start := timemodule.Now() @@ -7048,6 +7064,22 @@ func (s *TimerLayerThreadStore) UpdateMembership(membership *model.ThreadMembers return result, err } +func (s *TimerLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error { + start := timemodule.Now() + + err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.UpdateUnreadsByChannel", success, elapsed) + } + return err +} + func (s *TimerLayerTokenStore) Cleanup() { start := timemodule.Now() diff --git a/web/context_test.go b/web/context_test.go index 39f9dba242..6c27fc1b60 100644 --- a/web/context_test.go +++ b/web/context_test.go @@ -55,6 +55,8 @@ func TestMfaRequired(t *testing.T) { th.App.SetSession(&model.Session{Id: "abc", UserId: "userid"}) th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.AnnouncementSettings.UserNoticesEnabled = false + *cfg.AnnouncementSettings.AdminNoticesEnabled = false *cfg.ServiceSettings.EnableMultifactorAuthentication = true *cfg.ServiceSettings.EnforceMultifactorAuthentication = true })