diff --git a/api4/post.go b/api4/post.go index f52378082a..cada1fba2d 100644 --- a/api4/post.go +++ b/api4/post.go @@ -657,7 +657,7 @@ func setPostUnread(c *Context, w http.ResponseWriter, r *http.Request) { return } - state, err := c.App.MarkChannelAsUnreadFromPost(c.Params.PostId, c.Params.UserId, collapsedThreadsSupported) + state, err := c.App.MarkChannelAsUnreadFromPost(c.Params.PostId, c.Params.UserId, collapsedThreadsSupported, false) if err != nil { c.Err = err return diff --git a/api4/post_test.go b/api4/post_test.go index ed3400c3ae..43e9d7d61a 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -2547,33 +2547,6 @@ func TestSetChannelUnread(t *testing.T) { }) } -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(th.Context, &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(th.Context, &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, true) - 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) - -} - func TestSetPostUnreadWithoutCollapsedThreads(t *testing.T) { os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") diff --git a/app/app_iface.go b/app/app_iface.go index 4c850945ad..cdc0728a87 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -240,7 +240,7 @@ type AppIface interface { // MakeAuditRecord creates a audit record pre-populated with defaults. MakeAuditRecord(event string, initialStatus string) *audit.Record // MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one. - MarkChannelAsUnreadFromPost(postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) + MarkChannelAsUnreadFromPost(postID string, userID string, collapsedThreadsSupported, followThread bool) (*model.ChannelUnreadAt, *model.AppError) // MentionsToPublicChannels returns all the mentions to public channels, // linking them to their channels MentionsToPublicChannels(message, teamID string) model.ChannelMentionMap diff --git a/app/channel.go b/app/channel.go index 475d04d503..66c9cd3047 100644 --- a/app/channel.go +++ b/app/channel.go @@ -2467,7 +2467,7 @@ func (a *App) isCRTEnabledForUser(userID string) bool { } // MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one. -func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) { +func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string, collapsedThreadsSupported, followThread bool) (*model.ChannelUnreadAt, *model.AppError) { if !collapsedThreadsSupported || !a.isCRTEnabledForUser(userID) { return a.markChannelAsUnreadFromPostCRTUnsupported(postID, userID) } @@ -2502,7 +2502,7 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string, collapse // if this post was not followed before, create thread membership and update mention count if threadMembership == nil { opts := store.ThreadMembershipOpts{ - Following: true, + Following: followThread, IncrementMentions: true, UpdateFollowing: true, UpdateViewedTimestamp: true, @@ -2512,8 +2512,8 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string, collapse if storeErr != nil && !errors.As(storeErr, &nfErr) { return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, storeErr.Error(), http.StatusInternalServerError) } - threadData, storeErr := a.Srv().Store.Thread().Get(threadId) - if storeErr != nil && !errors.As(storeErr, &nfErr) { + threadData, storeErr2 := a.Srv().Store.Thread().Get(threadId) + if storeErr2 != nil && !errors.As(storeErr2, &nfErr) { return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, storeErr.Error(), http.StatusInternalServerError) } if threadData != nil && threadMembership != nil && threadMembership.Following { @@ -2541,6 +2541,15 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string, collapse message.Add("thread", payload) a.Publish(message) } + } else if !threadMembership.Following && followThread { + opts := store.ThreadMembershipOpts{ + Following: true, + UpdateFollowing: true, + } + _, storeErr = a.Srv().Store.Thread().MaintainMembership(user.Id, threadId, opts) + if storeErr != nil && !errors.As(storeErr, &nfErr) { + return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, storeErr.Error(), http.StatusInternalServerError) + } } } diff --git a/app/channel_test.go b/app/channel_test.go index ebd8667e58..5aacfba1db 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -1273,7 +1273,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) { require.Equal(t, int64(0), unread.MsgCount) t.Run("Unread but last one", func(t *testing.T) { - response, err := th.App.MarkChannelAsUnreadFromPost(p2.Id, u1.Id, true) + response, err := th.App.MarkChannelAsUnreadFromPost(p2.Id, u1.Id, true, true) require.Nil(t, err) require.NotNil(t, response) assert.Equal(t, int64(2), response.MsgCount) @@ -1284,7 +1284,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) { }) t.Run("Unread last one", func(t *testing.T) { - response, err := th.App.MarkChannelAsUnreadFromPost(p3.Id, u1.Id, true) + response, err := th.App.MarkChannelAsUnreadFromPost(p3.Id, u1.Id, true, true) require.Nil(t, err) require.NotNil(t, response) assert.Equal(t, int64(3), response.MsgCount) @@ -1295,7 +1295,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) { }) t.Run("Unread first one", func(t *testing.T) { - response, err := th.App.MarkChannelAsUnreadFromPost(p1.Id, u1.Id, true) + response, err := th.App.MarkChannelAsUnreadFromPost(p1.Id, u1.Id, true, true) require.Nil(t, err) require.NotNil(t, response) assert.Equal(t, int64(1), response.MsgCount) @@ -1312,7 +1312,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) { }) t.Run("Unread on a private channel", func(t *testing.T) { - response, err := th.App.MarkChannelAsUnreadFromPost(pp1.Id, u1.Id, true) + response, err := th.App.MarkChannelAsUnreadFromPost(pp1.Id, u1.Id, true, true) require.Nil(t, err) require.NotNil(t, response) assert.Equal(t, int64(0), response.MsgCount) @@ -1321,7 +1321,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) { assert.Equal(t, int64(2), unread.MsgCount) assert.Equal(t, pp1.CreateAt-1, response.LastViewedAt) - response, err = th.App.MarkChannelAsUnreadFromPost(pp2.Id, u1.Id, true) + response, err = th.App.MarkChannelAsUnreadFromPost(pp2.Id, u1.Id, true, true) assert.Nil(t, err) assert.Equal(t, int64(1), response.MsgCount) unread, err = th.App.GetChannelUnread(pc1.Id, u1.Id) @@ -1350,7 +1350,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) { Message: "@" + u1.Username, }, c2, false, true) - response, err := th.App.MarkChannelAsUnreadFromPost(p4.Id, u1.Id, true) + response, err := th.App.MarkChannelAsUnreadFromPost(p4.Id, u1.Id, true, true) assert.Nil(t, err) assert.Equal(t, int64(1), response.MsgCount) assert.Equal(t, int64(2), response.MentionCount) @@ -1373,7 +1373,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) { _, err := th.App.CreatePost(th.Context, &model.Post{ChannelId: dc.Id, UserId: th.BasicUser.Id, Message: "testReply", RootId: dm1.Id}, dc, false, false) assert.Nil(t, err) - response, err := th.App.MarkChannelAsUnreadFromPost(dm1.Id, u2.Id, true) + response, err := th.App.MarkChannelAsUnreadFromPost(dm1.Id, u2.Id, true, true) assert.Nil(t, err) assert.Equal(t, int64(0), response.MsgCount) assert.Equal(t, int64(4), response.MentionCount) @@ -1387,7 +1387,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) { }) t.Run("Can't unread an imaginary post", func(t *testing.T) { - response, err := th.App.MarkChannelAsUnreadFromPost("invalid4ofngungryquinj976y", u1.Id, true) + response, err := th.App.MarkChannelAsUnreadFromPost("invalid4ofngungryquinj976y", u1.Id, true, true) assert.NotNil(t, err) assert.Nil(t, response) }) @@ -2163,7 +2163,7 @@ func TestMarkChannelAsUnreadFromPostCollapsedThreadsTurnedOff(t *testing.T) { require.Nil(t, appErr) t.Run("Mark reply post as unread", func(t *testing.T) { - _, err := th.App.MarkChannelAsUnreadFromPost(replyPost1.Id, th.BasicUser.Id, true) + _, err := th.App.MarkChannelAsUnreadFromPost(replyPost1.Id, th.BasicUser.Id, true, true) require.Nil(t, err) // Get channel unreads // Easier to reason with ChannelUnread now, than channelUnreadAt from the previous call @@ -2187,7 +2187,7 @@ func TestMarkChannelAsUnreadFromPostCollapsedThreadsTurnedOff(t *testing.T) { }) t.Run("Mark root post as unread", func(t *testing.T) { - _, err := th.App.MarkChannelAsUnreadFromPost(rootPost1.Id, th.BasicUser.Id, true) + _, err := th.App.MarkChannelAsUnreadFromPost(rootPost1.Id, th.BasicUser.Id, true, true) require.Nil(t, err) // Get channel unreads // Easier to reason with ChannelUnread now, than channelUnreadAt from the previous call @@ -2201,3 +2201,48 @@ func TestMarkChannelAsUnreadFromPostCollapsedThreadsTurnedOff(t *testing.T) { require.Equal(t, int64(3), channelUnread.MsgCountRoot) }) } + +func TestMarkUnreadWithThreads(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(th.Context, &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(th.Context, &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, true, true) + 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) + + threadMembership, appErr := th.App.GetThreadMembershipForUser(th.BasicUser.Id, replyPost.RootId) + require.Nil(t, appErr) + require.NotNil(t, threadMembership) + assert.True(t, threadMembership.Following) + + // Create a new thread + rootPost, appErr = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "hi2"}, th.BasicChannel, false, false) + require.Nil(t, appErr) + replyPost, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "hi2"}, th.BasicChannel, false, false) + require.Nil(t, appErr) + + _, appErr = th.App.MarkChannelAsUnreadFromPost(replyPost.Id, th.BasicUser.Id, true, false) + require.Nil(t, appErr) + + threadMembership, appErr = th.App.GetThreadMembershipForUser(th.BasicUser.Id, replyPost.RootId) + require.Nil(t, appErr) + require.NotNil(t, threadMembership) + assert.False(t, threadMembership.Following) +} diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 333abcca89..6a740897ce 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -11332,7 +11332,7 @@ func (a *OpenTracingAppLayer) MakePermissionError(s *model.Session, permissions return resultVar0 } -func (a *OpenTracingAppLayer) MarkChannelAsUnreadFromPost(postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) { +func (a *OpenTracingAppLayer) MarkChannelAsUnreadFromPost(postID string, userID string, collapsedThreadsSupported bool, followThread bool) (*model.ChannelUnreadAt, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MarkChannelAsUnreadFromPost") @@ -11344,7 +11344,7 @@ func (a *OpenTracingAppLayer) MarkChannelAsUnreadFromPost(postID string, userID }() defer span.Finish() - resultVar0, resultVar1 := a.app.MarkChannelAsUnreadFromPost(postID, userID, collapsedThreadsSupported) + resultVar0, resultVar1 := a.app.MarkChannelAsUnreadFromPost(postID, userID, collapsedThreadsSupported, followThread) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) diff --git a/app/post_test.go b/app/post_test.go index da948924cd..6106eb98f5 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -2078,7 +2078,7 @@ func TestCollapsedThreadFetch(t *testing.T) { thread, nErr := th.App.Srv().Store.Thread().Get(postRoot.Id) require.NoError(t, nErr) require.Len(t, thread.Participants, 1) - th.App.MarkChannelAsUnreadFromPost(postRoot.Id, user1.Id, true) + th.App.MarkChannelAsUnreadFromPost(postRoot.Id, user1.Id, true, true) l, err := th.App.GetPostsForChannelAroundLastUnread(channel.Id, user1.Id, 10, 10, true, true, false) require.Nil(t, err) require.Len(t, l.Order, 1)