MM-34609 Mark-as-unread on a post in a thread should cause auto-follow (#17343)

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Eli Yukelzon
2021-04-16 10:26:08 +03:00
коммит произвёл GitHub
родитель 518e0ed371
Коммит f90209c8a3
11 изменённых файлов: 78 добавлений и 35 удалений

Просмотреть файл

@@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os"
"reflect" "reflect"
"sort" "sort"
"strings" "strings"
@@ -2544,3 +2545,30 @@ func TestSetChannelUnread(t *testing.T) {
checkHTTPStatus(t, response, http.StatusUnauthorized, true) checkHTTPStatus(t, response, http.StatusUnauthorized, true)
}) })
} }
func TestMarkUnreadCausesAutofollow(t *testing.T) {
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.COLLAPSED_THREADS_DEFAULT_ON
})
rootPost, appErr := th.App.CreatePost(&model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "hi"}, th.BasicChannel, false, false)
require.Nil(t, appErr)
replyPost, appErr := th.App.CreatePost(&model.Post{RootId: rootPost.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "hi"}, th.BasicChannel, false, false)
require.Nil(t, appErr)
threads, appErr := th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.Nil(t, appErr)
require.Zero(t, threads.Total)
_, appErr = th.App.MarkChannelAsUnreadFromPost(replyPost.Id, th.BasicUser.Id)
require.Nil(t, appErr)
threads, appErr = th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.Nil(t, appErr)
require.NotZero(t, threads.Total)
}

Просмотреть файл

@@ -2385,6 +2385,9 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string) (*model.
} }
threadMembership, _ := a.Srv().Store.Thread().GetMembershipForUser(user.Id, threadId) threadMembership, _ := a.Srv().Store.Thread().GetMembershipForUser(user.Id, threadId)
if threadMembership == nil {
threadMembership, _ = a.Srv().Store.Thread().MaintainMembership(user.Id, threadId, true, true, true, true)
}
if threadMembership != nil && threadMembership.Following { if threadMembership != nil && threadMembership.Following {
channel, nErr := a.Srv().Store.Channel().Get(post.ChannelId, true) channel, nErr := a.Srv().Store.Channel().Get(post.ChannelId, true)
if nErr != nil { if nErr != nil {

Просмотреть файл

@@ -185,7 +185,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
go func(userID string) { go func(userID string) {
defer close(mac) defer close(mac)
_, incrementMentions := mentions.Mentions[userID] _, incrementMentions := mentions.Mentions[userID]
err := a.Srv().Store.Thread().MaintainMembership(userID, post.RootId, true, incrementMentions, *a.Config().ServiceSettings.ThreadAutoFollow, userID == post.UserId) _, err := a.Srv().Store.Thread().MaintainMembership(userID, post.RootId, true, incrementMentions, *a.Config().ServiceSettings.ThreadAutoFollow, userID == post.UserId)
if err != nil { if err != nil {
mac <- model.NewAppError("SendNotifications", "app.channel.autofollow.app_error", nil, err.Error(), http.StatusInternalServerError) mac <- model.NewAppError("SendNotifications", "app.channel.autofollow.app_error", nil, err.Error(), http.StatusInternalServerError)
return return

Просмотреть файл

@@ -2424,7 +2424,7 @@ func (a *App) UpdateThreadsReadForUser(userID, teamID string) *model.AppError {
} }
func (a *App) UpdateThreadFollowForUser(userID, teamID, threadID string, state bool) *model.AppError { func (a *App) UpdateThreadFollowForUser(userID, teamID, threadID string, state bool) *model.AppError {
err := a.Srv().Store.Thread().MaintainMembership(userID, threadID, state, false, true, false) _, err := a.Srv().Store.Thread().MaintainMembership(userID, threadID, state, false, true, false)
if err != nil { if err != nil {
return model.NewAppError("UpdateThreadFollowForUser", "app.user.update_thread_follow_for_user.app_error", nil, err.Error(), http.StatusInternalServerError) return model.NewAppError("UpdateThreadFollowForUser", "app.user.update_thread_follow_for_user.app_error", nil, err.Error(), http.StatusInternalServerError)
} }

Просмотреть файл

@@ -8550,7 +8550,7 @@ func (s *OpenTracingLayerThreadStore) GetThreadsForUser(userId string, teamID st
return result, err return result, err
} }
func (s *OpenTracingLayerThreadStore) MaintainMembership(userID string, postID string, following bool, incrementMentions bool, updateFollowing bool, updateViewedTimestamp bool) error { func (s *OpenTracingLayerThreadStore) MaintainMembership(userID string, postID string, following bool, incrementMentions bool, updateFollowing bool, updateViewedTimestamp bool) (*model.ThreadMembership, error) {
origCtx := s.Root.Store.Context() origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MaintainMembership") span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MaintainMembership")
s.Root.Store.SetContext(newCtx) s.Root.Store.SetContext(newCtx)
@@ -8559,13 +8559,13 @@ func (s *OpenTracingLayerThreadStore) MaintainMembership(userID string, postID s
}() }()
defer span.Finish() defer span.Finish()
err := s.ThreadStore.MaintainMembership(userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp) result, err := s.ThreadStore.MaintainMembership(userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp)
if err != nil { if err != nil {
span.LogFields(spanlog.Error(err)) span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true) ext.Error.Set(span, true)
} }
return err return result, err
} }
func (s *OpenTracingLayerThreadStore) MarkAllAsRead(userID string, teamID string) error { func (s *OpenTracingLayerThreadStore) MarkAllAsRead(userID string, teamID string) error {

Просмотреть файл

@@ -9298,21 +9298,21 @@ func (s *RetryLayerThreadStore) GetThreadsForUser(userId string, teamID string,
} }
func (s *RetryLayerThreadStore) MaintainMembership(userID string, postID string, following bool, incrementMentions bool, updateFollowing bool, updateViewedTimestamp bool) error { func (s *RetryLayerThreadStore) MaintainMembership(userID string, postID string, following bool, incrementMentions bool, updateFollowing bool, updateViewedTimestamp bool) (*model.ThreadMembership, error) {
tries := 0 tries := 0
for { for {
err := s.ThreadStore.MaintainMembership(userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp) result, err := s.ThreadStore.MaintainMembership(userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp)
if err == nil { if err == nil {
return nil return result, nil
} }
if !isRepeatableError(err) { if !isRepeatableError(err) {
return err return result, err
} }
tries++ tries++
if tries >= 3 { if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err return result, err
} }
} }

Просмотреть файл

@@ -472,7 +472,7 @@ func (s *SqlThreadStore) DeleteMembershipForUser(userId string, postId string) e
return nil return nil
} }
func (s *SqlThreadStore) MaintainMembership(userId, postId string, following, incrementMentions, updateFollowing, updateViewedTimestamp bool) error { func (s *SqlThreadStore) MaintainMembership(userId, postId string, following, incrementMentions, updateFollowing, updateViewedTimestamp bool) (*model.ThreadMembership, error) {
membership, err := s.GetMembershipForUser(userId, postId) membership, err := s.GetMembershipForUser(userId, postId)
now := utils.MillisFromTime(time.Now()) now := utils.MillisFromTime(time.Now())
// if memebership exists, update it if: // if memebership exists, update it if:
@@ -494,19 +494,19 @@ func (s *SqlThreadStore) MaintainMembership(userId, postId string, following, in
} }
_, err = s.UpdateMembership(membership) _, err = s.UpdateMembership(membership)
} }
return err return nil, err
} }
var nfErr *store.ErrNotFound var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) { if !errors.As(err, &nfErr) {
return errors.Wrap(err, "failed to get thread membership") return nil, errors.Wrap(err, "failed to get thread membership")
} }
mentions := 0 mentions := 0
if incrementMentions { if incrementMentions {
mentions = 1 mentions = 1
} }
_, err = s.SaveMembership(&model.ThreadMembership{ membership, err = s.SaveMembership(&model.ThreadMembership{
PostId: postId, PostId: postId,
UserId: userId, UserId: userId,
Following: following, Following: following,
@@ -515,18 +515,18 @@ func (s *SqlThreadStore) MaintainMembership(userId, postId string, following, in
UnreadMentions: int64(mentions), UnreadMentions: int64(mentions),
}) })
if err != nil { if err != nil {
return err return nil, err
} }
thread, err := s.Get(postId) thread, err := s.Get(postId)
if err != nil { if err != nil {
return err return nil, err
} }
if !thread.Participants.Contains(userId) { if !thread.Participants.Contains(userId) {
thread.Participants = append(thread.Participants, userId) thread.Participants = append(thread.Participants, userId)
_, err = s.Update(thread) _, err = s.Update(thread)
} }
return err return membership, err
} }
func (s *SqlThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) { func (s *SqlThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) {

Просмотреть файл

@@ -272,7 +272,7 @@ type ThreadStore interface {
GetMembershipsForUser(userId, teamID string) ([]*model.ThreadMembership, error) GetMembershipsForUser(userId, teamID string) ([]*model.ThreadMembership, error)
GetMembershipForUser(userId, postID string) (*model.ThreadMembership, error) GetMembershipForUser(userId, postID string) (*model.ThreadMembership, error)
DeleteMembershipForUser(userId, postID string) error DeleteMembershipForUser(userId, postID string) error
MaintainMembership(userID, postID string, following, incrementMentions, updateFollowing, updateViewedTimestamp bool) error MaintainMembership(userID, postID string, following, incrementMentions, updateFollowing, updateViewedTimestamp bool) (*model.ThreadMembership, error)
CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error)
UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64, updateViewedTimestamp bool) error UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64, updateViewedTimestamp bool) error
} }

Просмотреть файл

@@ -204,17 +204,26 @@ func (_m *ThreadStore) GetThreadsForUser(userId string, teamID string, opts mode
} }
// MaintainMembership provides a mock function with given fields: userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp // MaintainMembership provides a mock function with given fields: userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp
func (_m *ThreadStore) MaintainMembership(userID string, postID string, following bool, incrementMentions bool, updateFollowing bool, updateViewedTimestamp bool) error { func (_m *ThreadStore) MaintainMembership(userID string, postID string, following bool, incrementMentions bool, updateFollowing bool, updateViewedTimestamp bool) (*model.ThreadMembership, error) {
ret := _m.Called(userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp) ret := _m.Called(userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp)
var r0 error var r0 *model.ThreadMembership
if rf, ok := ret.Get(0).(func(string, string, bool, bool, bool, bool) error); ok { if rf, ok := ret.Get(0).(func(string, string, bool, bool, bool, bool) *model.ThreadMembership); ok {
r0 = rf(userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp) r0 = rf(userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp)
} else { } else {
r0 = ret.Error(0) if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ThreadMembership)
}
} }
return r0 var r1 error
if rf, ok := ret.Get(1).(func(string, string, bool, bool, bool, bool) error); ok {
r1 = rf(userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp)
} else {
r1 = ret.Error(1)
}
return r0, r1
} }
// MarkAllAsRead provides a mock function with given fields: userID, teamID // MarkAllAsRead provides a mock function with given fields: userID, teamID

Просмотреть файл

@@ -235,8 +235,8 @@ 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) { t.Run("Thread last updated is changed when channel is updated after UpdateLastViewedAtPost", func(t *testing.T) {
newPosts := makeSomePosts() newPosts := makeSomePosts()
_, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, false)
require.NoError(t, ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, false)) require.NoError(t, e)
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err1) require.NoError(t, err1)
m.LastUpdated -= 1000 m.LastUpdated -= 1000
@@ -256,7 +256,8 @@ 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) { t.Run("Thread last updated is changed when channel is updated after IncrementMentionCount", func(t *testing.T) {
newPosts := makeSomePosts() newPosts := makeSomePosts()
require.NoError(t, ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, false)) _, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, false)
require.NoError(t, e)
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err1) require.NoError(t, err1)
m.LastUpdated -= 1000 m.LastUpdated -= 1000
@@ -275,8 +276,8 @@ 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) { t.Run("Thread last updated is changed when channel is updated after UpdateLastViewedAt", func(t *testing.T) {
newPosts := makeSomePosts() newPosts := makeSomePosts()
_, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, false)
require.NoError(t, ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, false)) require.NoError(t, e)
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err1) require.NoError(t, err1)
m.LastUpdated -= 1000 m.LastUpdated -= 1000
@@ -296,12 +297,14 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) {
t.Run("Thread membership 'viewed' timestamp is updated properly", func(t *testing.T) { t.Run("Thread membership 'viewed' timestamp is updated properly", func(t *testing.T) {
newPosts := makeSomePosts() newPosts := makeSomePosts()
require.NoError(t, ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, false)) _, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, false)
require.NoError(t, e)
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err1) require.NoError(t, err1)
require.Equal(t, int64(0), m.LastViewed) require.Equal(t, int64(0), m.LastViewed)
require.NoError(t, ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, true)) _, e = ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, true)
require.NoError(t, e)
m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err2) require.NoError(t, err2)
require.Greater(t, m2.LastViewed, int64(0)) require.Greater(t, m2.LastViewed, int64(0))
@@ -309,8 +312,8 @@ 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) { t.Run("Thread last updated is changed when channel is updated after UpdateLastViewedAtPost for mark unread", func(t *testing.T) {
newPosts := makeSomePosts() newPosts := makeSomePosts()
_, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, false)
require.NoError(t, ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, true, false, true, false)) require.NoError(t, e)
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id) m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err1) require.NoError(t, err1)
m.LastUpdated += 1000 m.LastUpdated += 1000

Просмотреть файл

@@ -7708,10 +7708,10 @@ func (s *TimerLayerThreadStore) GetThreadsForUser(userId string, teamID string,
return result, err return result, err
} }
func (s *TimerLayerThreadStore) MaintainMembership(userID string, postID string, following bool, incrementMentions bool, updateFollowing bool, updateViewedTimestamp bool) error { func (s *TimerLayerThreadStore) MaintainMembership(userID string, postID string, following bool, incrementMentions bool, updateFollowing bool, updateViewedTimestamp bool) (*model.ThreadMembership, error) {
start := timemodule.Now() start := timemodule.Now()
err := s.ThreadStore.MaintainMembership(userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp) result, err := s.ThreadStore.MaintainMembership(userID, postID, following, incrementMentions, updateFollowing, updateViewedTimestamp)
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
if s.Root.Metrics != nil { if s.Root.Metrics != nil {
@@ -7721,7 +7721,7 @@ func (s *TimerLayerThreadStore) MaintainMembership(userID string, postID string,
} }
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MaintainMembership", success, elapsed) s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MaintainMembership", success, elapsed)
} }
return err return result, err
} }
func (s *TimerLayerThreadStore) MarkAllAsRead(userID string, teamID string) error { func (s *TimerLayerThreadStore) MarkAllAsRead(userID string, teamID string) error {