diff --git a/api4/user_test.go b/api4/user_test.go index 93d3f33ded..465f600a5b 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -5544,6 +5544,80 @@ func TestFollowThreads(t *testing.T) { }) } +func postAndCheck(t *testing.T, client *model.Client4, post *model.Post) (*model.Post, *model.Response) { + p, resp := client.CreatePost(post) + CheckNoError(t, resp) + CheckCreatedStatus(t, resp) + return p, resp +} + +func TestMaintainUnreadMentionsInThread(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + Client := th.Client + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ThreadAutoFollow = true + *cfg.ServiceSettings.CollapsedThreads = model.COLLAPSED_THREADS_DEFAULT_ON + }) + + checkThreadList := func(client *model.Client4, userId string, expectedMentions, expectedThreads int) (*model.Threads, *model.Response) { + uss, resp := client.GetUserThreads(userId, model.GetUserThreadsOpts{ + Page: 0, + PageSize: 30, + Deleted: false, + }) + CheckNoError(t, resp) + require.Len(t, uss.Threads, expectedThreads) + + // validate amount of mentions via store. once GetUserThreads starts returning mentions - update + memberships, err := th.App.Srv().Store.Thread().GetMembershipsForUser(userId) + require.NoError(t, err) + sum := int64(0) + for _, membership := range memberships { + sum += membership.UnreadMentions + } + require.EqualValues(t, expectedMentions, sum) + return uss, resp + } + + // create regular post + rpost, _ := postAndCheck(t, Client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"}) + // create reply and mention the original poster and another user + postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply @" + th.BasicUser.Username + " and @" + th.BasicUser2.Username, RootId: rpost.Id}) + defer th.App.Srv().Store.Post().PermanentDeleteByUser(th.BasicUser.Id) + defer th.App.Srv().Store.Post().PermanentDeleteByUser(th.SystemAdminUser.Id) + + // basic user 1 was mentioned 1 time + checkThreadList(th.Client, th.BasicUser.Id, 1, 1) + // basic user 2 was mentioned 1 time + checkThreadList(th.SystemAdminClient, th.BasicUser2.Id, 1, 1) + + // test self mention, shouldn't increase mention count + postAndCheck(t, Client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply @" + th.BasicUser.Username, RootId: rpost.Id}) + // count should increase + checkThreadList(th.Client, th.BasicUser.Id, 1, 1) + + // test DM + dm := th.CreateDmChannel(th.SystemAdminUser) + dm_root_post, _ := postAndCheck(t, Client, &model.Post{ChannelId: dm.Id, Message: "hi @" + th.SystemAdminUser.Username}) + + // no changes + checkThreadList(th.Client, th.BasicUser.Id, 1, 1) + + // post reply by the same user + postAndCheck(t, Client, &model.Post{ChannelId: dm.Id, Message: "how are you", RootId: dm_root_post.Id}) + + // thread created + checkThreadList(th.Client, th.BasicUser.Id, 1, 2) + + // post two replies by another user, without mentions. mention count should still increase since this is a DM + postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: dm.Id, Message: "msg1", RootId: dm_root_post.Id}) + postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: dm.Id, Message: "msg2", RootId: dm_root_post.Id}) + // expect increment by two mentions + checkThreadList(th.Client, th.BasicUser.Id, 3, 2) + +} + func TestReadThreads(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/app/channel.go b/app/channel.go index 8c12a68bf4..8805704934 100644 --- a/app/channel.go +++ b/app/channel.go @@ -2337,6 +2337,24 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string) (*model. return nil, err } + if *a.Config().ServiceSettings.ThreadAutoFollow && post.RootId != "" { + threadMembership, _ := a.Srv().Store.Thread().GetMembershipForUser(user.Id, post.RootId) + if threadMembership != nil { + channel, nErr := a.Srv().Store.Channel().Get(post.ChannelId, true) + if nErr != nil { + return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } + threadMembership.UnreadMentions, err = a.countThreadMentions(user, post, channel.TeamId, post.UpdateAt-1) + if err != nil { + return nil, err + } + _, nErr = a.Srv().Store.Thread().UpdateMembership(threadMembership) + if nErr != nil { + return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } + } + } + 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) diff --git a/app/notification.go b/app/notification.go index 02489281b1..e2567bee40 100644 --- a/app/notification.go +++ b/app/notification.go @@ -162,21 +162,27 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod mentionedUsersList := make([]string, 0, len(mentions.Mentions)) updateMentionChans := []chan *model.AppError{} mentionAutofollowChans := []chan *model.AppError{} - threadParticipants := []string{post.UserId} + threadParticipants := map[string]bool{post.UserId: true} if *a.Config().ServiceSettings.ThreadAutoFollow && post.RootId != "" { if parentPostList != nil { - threadParticipants = append(threadParticipants, parentPostList.Posts[parentPostList.Order[0]].UserId) + threadParticipants[parentPostList.Posts[parentPostList.Order[0]].UserId] = true } for id := range mentions.Mentions { - threadParticipants = append(threadParticipants, id) + threadParticipants[id] = true } - // for each mention, make sure to update thread autofollow - for _, id := range threadParticipants { + // for each mention, make sure to update thread autofollow (if enabled) and update increment mention count + for id := range threadParticipants { mac := make(chan *model.AppError, 1) go func(userId string) { defer close(mac) - - nErr := a.Srv().Store.Thread().CreateMembershipIfNeeded(userId, post.RootId, true) + incrementMentions := false + for mid := range mentions.Mentions { + if userId == mid { + incrementMentions = true + break + } + } + nErr := a.Srv().Store.Thread().CreateMembershipIfNeeded(userId, post.RootId, true, incrementMentions, *a.Config().ServiceSettings.ThreadAutoFollow) if nErr != nil { mac <- model.NewAppError("SendNotifications", "app.channel.autofollow.app_error", nil, nErr.Error(), http.StatusInternalServerError) return diff --git a/app/post.go b/app/post.go index 5b19ec16af..e8cddc31f9 100644 --- a/app/post.go +++ b/app/post.go @@ -458,7 +458,7 @@ func (a *App) handlePostEvents(post *model.Post, user *model.User, channel *mode } if *a.Config().ServiceSettings.ThreadAutoFollow && post.RootId != "" { - if err := a.Srv().Store.Thread().CreateMembershipIfNeeded(post.UserId, post.RootId, true); err != nil { + if err := a.Srv().Store.Thread().CreateMembershipIfNeeded(post.UserId, post.RootId, true, false, true); err != nil { return err } } @@ -1340,6 +1340,64 @@ func (a *App) MaxPostSize() int { return a.Srv().MaxPostSize() } +// countThreadMentions returns the number of times the user is mentioned in a specified thread after the timestamp. +func (a *App) countThreadMentions(user *model.User, post *model.Post, teamId string, timestamp int64) (int64, *model.AppError) { + team, err := a.GetTeam(teamId) + if err != nil { + return 0, err + } + channel, err := a.GetChannel(post.ChannelId) + if err != nil { + return 0, err + } + + keywords := addMentionKeywordsForUser( + map[string][]string{}, + user, + map[string]string{}, + &model.Status{Status: model.STATUS_ONLINE}, // Assume the user is online since they would've triggered this + true, // Assume channel mentions are always allowed for simplicity + ) + + posts, nErr := a.Srv().Store.Thread().GetPosts(post.Id, timestamp) + if nErr != nil { + return 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } + + count := 0 + + if channel.Type == model.CHANNEL_DIRECT { + // In a DM channel, every post made by the other user is a mention + otherId := channel.GetOtherUserIdForDM(user.Id) + for _, p := range posts { + if p.UserId == otherId { + count++ + } + } + + return int64(count), nil + } + + groups, nErr := a.getGroupsAllowedForReferenceInChannel(channel, team) + if nErr != nil { + return 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } + + mentions := getExplicitMentions(post, keywords, groups) + if _, ok := mentions.Mentions[user.Id]; ok { + count += 1 + } + + for _, p := range posts { + mentions = getExplicitMentions(p, keywords, groups) + if _, ok := mentions.Mentions[user.Id]; ok { + count += 1 + } + } + + return int64(count), nil +} + // 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(user *model.User, post *model.Post) (int, *model.AppError) { diff --git a/app/user.go b/app/user.go index 9be5a2799d..dda7bed393 100644 --- a/app/user.go +++ b/app/user.go @@ -2384,9 +2384,9 @@ func (a *App) GetThreadsForUser(userId string, options model.GetUserThreadsOpts) } func (a *App) UpdateThreadsReadForUser(userId string, timestamp int64) *model.AppError { - err := a.Srv().Store.Thread().MarkAllAsRead(userId, timestamp) - if err != nil { - return model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, err.Error(), http.StatusInternalServerError) + nErr := a.Srv().Store.Thread().MarkAllAsRead(userId, timestamp) + if nErr != nil { + return model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError) } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_READ_CHANGED, "", "", userId, nil) message.Add("timestamp", timestamp) @@ -2395,7 +2395,7 @@ func (a *App) UpdateThreadsReadForUser(userId string, timestamp int64) *model.Ap } func (a *App) UpdateThreadFollowForUser(userId, threadId string, state bool) *model.AppError { - err := a.Srv().Store.Thread().CreateMembershipIfNeeded(userId, threadId, state) + err := a.Srv().Store.Thread().CreateMembershipIfNeeded(userId, threadId, state, false, true) if err != nil { return model.NewAppError("UpdateThreadFollowForUser", "app.user.update_thread_follow_for_user.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -2407,9 +2407,9 @@ func (a *App) UpdateThreadFollowForUser(userId, threadId string, state bool) *mo } func (a *App) UpdateThreadReadForUser(userId, threadId string, timestamp int64) *model.AppError { - err := a.Srv().Store.Thread().MarkAsRead(userId, threadId, timestamp) - if err != nil { - return model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, err.Error(), http.StatusInternalServerError) + nErr := a.Srv().Store.Thread().MarkAsRead(userId, threadId, timestamp) + if nErr != nil { + return model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError) } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_READ_CHANGED, "", "", userId, nil) message.Add("thread_id", threadId) diff --git a/model/thread.go b/model/thread.go index 3c79b043a8..eb4b7faf8e 100644 --- a/model/thread.go +++ b/model/thread.go @@ -67,11 +67,12 @@ func (o *Thread) Etag() string { } type ThreadMembership struct { - PostId string `json:"post_id"` - UserId string `json:"user_id"` - Following bool `json:"following"` - LastViewed int64 `json:"last_view_at"` - LastUpdated int64 `json:"last_update_at"` + PostId string `json:"post_id"` + UserId string `json:"user_id"` + Following bool `json:"following"` + LastViewed int64 `json:"last_view_at"` + LastUpdated int64 `json:"last_update_at"` + UnreadMentions int64 `json:"unread_mentions"` } func (o *ThreadMembership) ToJson() string { diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index ad60a41a4d..f7e43383d7 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -7666,7 +7666,7 @@ func (s *OpenTracingLayerThreadStore) CollectThreadsWithNewerReplies(userId stri return result, err } -func (s *OpenTracingLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string, following bool) error { +func (s *OpenTracingLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string, following bool, incrementMentions bool, updateFollowing bool) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.CreateMembershipIfNeeded") s.Root.Store.SetContext(newCtx) @@ -7675,7 +7675,7 @@ func (s *OpenTracingLayerThreadStore) CreateMembershipIfNeeded(userId string, po }() defer span.Finish() - err := s.ThreadStore.CreateMembershipIfNeeded(userId, postId, following) + err := s.ThreadStore.CreateMembershipIfNeeded(userId, postId, following, incrementMentions, updateFollowing) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -7774,6 +7774,24 @@ func (s *OpenTracingLayerThreadStore) GetMembershipsForUser(userId string) ([]*m return result, err } +func (s *OpenTracingLayerThreadStore) GetPosts(threadId string, since int64) ([]*model.Post, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetPosts") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ThreadStore.GetPosts(threadId, since) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerThreadStore) GetThreadsForUser(userId string, opts model.GetUserThreadsOpts) (*model.Threads, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadsForUser") @@ -7918,7 +7936,7 @@ func (s *OpenTracingLayerThreadStore) UpdateMembership(membership *model.ThreadM return result, err } -func (s *OpenTracingLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error { +func (s *OpenTracingLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64, updateViewedTimestamp bool) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.UpdateUnreadsByChannel") s.Root.Store.SetContext(newCtx) @@ -7927,7 +7945,7 @@ func (s *OpenTracingLayerThreadStore) UpdateUnreadsByChannel(userId string, chan }() defer span.Finish() - err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp) + err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp, updateViewedTimestamp) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index a566dbd9ed..2f54f15b79 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -8318,11 +8318,11 @@ func (s *RetryLayerThreadStore) CollectThreadsWithNewerReplies(userId string, ch } -func (s *RetryLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string, following bool) error { +func (s *RetryLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string, following bool, incrementMentions bool, updateFollowing bool) error { tries := 0 for { - err := s.ThreadStore.CreateMembershipIfNeeded(userId, postId, following) + err := s.ThreadStore.CreateMembershipIfNeeded(userId, postId, following, incrementMentions, updateFollowing) if err == nil { return nil } @@ -8438,6 +8438,26 @@ func (s *RetryLayerThreadStore) GetMembershipsForUser(userId string) ([]*model.T } +func (s *RetryLayerThreadStore) GetPosts(threadId string, since int64) ([]*model.Post, error) { + + tries := 0 + for { + result, err := s.ThreadStore.GetPosts(threadId, since) + 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) GetThreadsForUser(userId string, opts model.GetUserThreadsOpts) (*model.Threads, error) { tries := 0 @@ -8598,11 +8618,11 @@ func (s *RetryLayerThreadStore) UpdateMembership(membership *model.ThreadMembers } -func (s *RetryLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error { +func (s *RetryLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64, updateViewedTimestamp bool) error { tries := 0 for { - err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp) + err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp, updateViewedTimestamp) if err == nil { return nil } diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 69bba27cb3..176c51ec34 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -2100,7 +2100,7 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string, times[t.Id] = t.LastPostAt } if updateThreads { - s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now) + s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now, true) } return times, nil } @@ -2136,7 +2136,7 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string, } if updateThreads { - s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now) + s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now, true) } return times, nil } @@ -2252,7 +2252,7 @@ func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID s } if updateThreads { - s.Thread().UpdateUnreadsByChannel(userID, threadsToUpdate, unreadDate) + s.Thread().UpdateUnreadsByChannel(userID, threadsToUpdate, unreadDate, true) } return result, nil } @@ -2282,7 +2282,7 @@ func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string, return errors.Wrapf(err, "failed to Update ChannelMembers with channelId=%s and userId=%s", channelId, userId) } if updateThreads { - s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now) + s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now, false) } return nil } diff --git a/store/sqlstore/thread_store.go b/store/sqlstore/thread_store.go index b63caab976..a7b671a1c8 100644 --- a/store/sqlstore/thread_store.go +++ b/store/sqlstore/thread_store.go @@ -120,7 +120,7 @@ func (s *SqlThreadStore) GetThreadsForUser(userId string, opts model.GetUserThre var threads []*JoinedThread fetchConditions := sq.And{ - sq.Eq{"Posts.UserId": userId}, + sq.Eq{"ThreadMemberships.UserId": userId}, sq.Eq{"ThreadMemberships.Following": true}, } if !opts.Deleted { @@ -273,13 +273,18 @@ func (s *SqlThreadStore) DeleteMembershipForUser(userId string, postId string) e return nil } -func (s *SqlThreadStore) CreateMembershipIfNeeded(userId, postId string, following bool) error { +func (s *SqlThreadStore) CreateMembershipIfNeeded(userId, postId string, following, incrementMentions, updateFollowing bool) error { membership, err := s.GetMembershipForUser(userId, postId) now := utils.MillisFromTime(time.Now()) if err == nil { - if !membership.Following || membership.Following != following { - membership.Following = following + if (updateFollowing && !membership.Following || membership.Following != following) || incrementMentions { + if updateFollowing { + membership.Following = following + } membership.LastUpdated = now + if incrementMentions { + membership.UnreadMentions += 1 + } _, err = s.UpdateMembership(membership) } return err @@ -290,12 +295,17 @@ func (s *SqlThreadStore) CreateMembershipIfNeeded(userId, postId string, followi if !errors.As(err, &nfErr) { return errors.Wrap(err, "failed to get thread membership") } + mentions := 0 + if incrementMentions { + mentions = 1 + } _, err = s.SaveMembership(&model.ThreadMembership{ - PostId: postId, - UserId: userId, - Following: following, - LastViewed: 0, - LastUpdated: now, + PostId: postId, + UserId: userId, + Following: following, + LastViewed: 0, + LastUpdated: now, + UnreadMentions: int64(mentions), }) return err } @@ -321,19 +331,38 @@ func (s *SqlThreadStore) CollectThreadsWithNewerReplies(userId string, channelId return changedThreads, nil } -func (s *SqlThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error { +func (s *SqlThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64, updateViewedTimestamp bool) error { if len(changedThreads) == 0 { return nil } - updateQuery, updateArgs, _ := s.getQueryBuilder(). + + qb := s.getQueryBuilder(). Update("ThreadMemberships"). Where(sq.Eq{"UserId": userId, "PostId": changedThreads}). - Set("LastUpdated", timestamp). - Set("LastViewed", timestamp). - ToSql() + Set("LastUpdated", timestamp) + + if updateViewedTimestamp { + qb = qb.Set("LastViewed", timestamp) + } + updateQuery, updateArgs, _ := qb.ToSql() + if _, err := s.GetMaster().Exec(updateQuery, updateArgs...); err != nil { return errors.Wrap(err, "failed to update thread membership") } return nil } + +func (s *SqlThreadStore) GetPosts(threadId string, since int64) ([]*model.Post, error) { + query, args, _ := s.getQueryBuilder(). + Select("*"). + From("Posts"). + Where(sq.Eq{"RootId": threadId}). + Where(sq.Eq{"DeleteAt": 0}). + Where(sq.GtOrEq{"UpdateAt": since}).ToSql() + var result []*model.Post + if _, err := s.GetReplica().Select(&result, query, args...); err != nil { + return nil, errors.Wrap(err, "failed to fetch thread posts") + } + return result, nil +} diff --git a/store/store.go b/store/store.go index 625c165b37..912936027e 100644 --- a/store/store.go +++ b/store/store.go @@ -253,6 +253,7 @@ type ThreadStore interface { Get(id string) (*model.Thread, error) GetThreadsForUser(userId string, opts model.GetUserThreadsOpts) (*model.Threads, error) Delete(postId string) error + GetPosts(threadId string, since int64) ([]*model.Post, error) MarkAllAsRead(userId string, timestamp int64) error MarkAsRead(userId, threadId string, timestamp int64) error @@ -262,9 +263,9 @@ type ThreadStore interface { GetMembershipsForUser(userId string) ([]*model.ThreadMembership, error) GetMembershipForUser(userId, postId string) (*model.ThreadMembership, error) DeleteMembershipForUser(userId, postId string) error - CreateMembershipIfNeeded(userId, postId string, following bool) error + CreateMembershipIfNeeded(userId, postId string, following, incrementMentions, updateFollowing bool) error CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) - UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error + UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64, updateViewedTimestamp bool) error } type PostStore interface { diff --git a/store/storetest/mocks/ThreadStore.go b/store/storetest/mocks/ThreadStore.go index c56a709722..60d6674dc4 100644 --- a/store/storetest/mocks/ThreadStore.go +++ b/store/storetest/mocks/ThreadStore.go @@ -37,13 +37,13 @@ func (_m *ThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds return r0, r1 } -// CreateMembershipIfNeeded provides a mock function with given fields: userId, postId, following -func (_m *ThreadStore) CreateMembershipIfNeeded(userId string, postId string, following bool) error { - ret := _m.Called(userId, postId, following) +// CreateMembershipIfNeeded provides a mock function with given fields: userId, postId, following, incrementMentions, updateFollowing +func (_m *ThreadStore) CreateMembershipIfNeeded(userId string, postId string, following bool, incrementMentions bool, updateFollowing bool) error { + ret := _m.Called(userId, postId, following, incrementMentions, updateFollowing) var r0 error - if rf, ok := ret.Get(0).(func(string, string, bool) error); ok { - r0 = rf(userId, postId, following) + if rf, ok := ret.Get(0).(func(string, string, bool, bool, bool) error); ok { + r0 = rf(userId, postId, following, incrementMentions, updateFollowing) } else { r0 = ret.Error(0) } @@ -148,6 +148,29 @@ func (_m *ThreadStore) GetMembershipsForUser(userId string) ([]*model.ThreadMemb return r0, r1 } +// GetPosts provides a mock function with given fields: threadId, since +func (_m *ThreadStore) GetPosts(threadId string, since int64) ([]*model.Post, error) { + ret := _m.Called(threadId, since) + + var r0 []*model.Post + if rf, ok := ret.Get(0).(func(string, int64) []*model.Post); ok { + r0 = rf(threadId, since) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Post) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, int64) error); ok { + r1 = rf(threadId, since) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetThreadsForUser provides a mock function with given fields: userId, opts func (_m *ThreadStore) GetThreadsForUser(userId string, opts model.GetUserThreadsOpts) (*model.Threads, error) { ret := _m.Called(userId, opts) @@ -321,13 +344,13 @@ 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) +// UpdateUnreadsByChannel provides a mock function with given fields: userId, changedThreads, timestamp, updateViewedTimestamp +func (_m *ThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64, updateViewedTimestamp bool) error { + ret := _m.Called(userId, changedThreads, timestamp, updateViewedTimestamp) var r0 error - if rf, ok := ret.Get(0).(func(string, []string, int64) error); ok { - r0 = rf(userId, changedThreads, timestamp) + if rf, ok := ret.Get(0).(func(string, []string, int64, bool) error); ok { + r0 = rf(userId, changedThreads, timestamp, updateViewedTimestamp) } else { r0 = ret.Error(0) } diff --git a/store/storetest/thread_store.go b/store/storetest/thread_store.go index 38d87399b3..e8fb28b167 100644 --- a/store/storetest/thread_store.go +++ b/store/storetest/thread_store.go @@ -234,7 +234,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { 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, true)) + require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id, true, false, true)) m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) require.Nil(t, err1) m.LastUpdated -= 1000 @@ -254,7 +254,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { 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, true)) + require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id, true, false, true)) m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) require.Nil(t, err1) m.LastUpdated -= 1000 @@ -274,7 +274,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { 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, true)) + require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id, true, false, true)) m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) require.Nil(t, err1) m.LastUpdated -= 1000 @@ -294,7 +294,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { 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, true)) + require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id, true, false, true)) m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) require.Nil(t, err1) m.LastUpdated += 1000 diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 8ffb2514da..4a48c6a0a1 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -6920,10 +6920,10 @@ func (s *TimerLayerThreadStore) CollectThreadsWithNewerReplies(userId string, ch return result, err } -func (s *TimerLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string, following bool) error { +func (s *TimerLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string, following bool, incrementMentions bool, updateFollowing bool) error { start := timemodule.Now() - err := s.ThreadStore.CreateMembershipIfNeeded(userId, postId, following) + err := s.ThreadStore.CreateMembershipIfNeeded(userId, postId, following, incrementMentions, updateFollowing) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) if s.Root.Metrics != nil { @@ -7016,6 +7016,22 @@ func (s *TimerLayerThreadStore) GetMembershipsForUser(userId string) ([]*model.T return result, err } +func (s *TimerLayerThreadStore) GetPosts(threadId string, since int64) ([]*model.Post, error) { + start := timemodule.Now() + + result, err := s.ThreadStore.GetPosts(threadId, since) + + 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.GetPosts", success, elapsed) + } + return result, err +} + func (s *TimerLayerThreadStore) GetThreadsForUser(userId string, opts model.GetUserThreadsOpts) (*model.Threads, error) { start := timemodule.Now() @@ -7144,10 +7160,10 @@ func (s *TimerLayerThreadStore) UpdateMembership(membership *model.ThreadMembers return result, err } -func (s *TimerLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error { +func (s *TimerLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64, updateViewedTimestamp bool) error { start := timemodule.Now() - err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp) + err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp, updateViewedTimestamp) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) if s.Root.Metrics != nil {