diff --git a/api4/user.go b/api4/user.go index 8863f873f6..748950da7a 100644 --- a/api4/user.go +++ b/api4/user.go @@ -102,6 +102,7 @@ func (api *API) InitUser() { api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(followThreadByUser)).Methods("PUT") api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(unfollowThreadByUser)).Methods("DELETE") api.BaseRoutes.UserThread.Handle("/read/{timestamp:[0-9]+}", api.APISessionRequired(updateReadStateThreadByUser)).Methods("PUT") + api.BaseRoutes.UserThread.Handle("/set_unread/{post_id:[A-Za-z0-9]+}", api.APISessionRequired(setUnreadThreadByPostId)).Methods("POST") } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { @@ -3097,6 +3098,42 @@ func updateReadStateThreadByUser(c *Context, w http.ResponseWriter, r *http.Requ auditRec.Success() } +func setUnreadThreadByPostId(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId().RequireThreadId().RequirePostId().RequireTeamId() + if c.Err != nil { + return + } + + auditRec := c.MakeAuditRecord("setUnreadThreadByPostId", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("user_id", c.Params.UserId) + auditRec.AddMeta("thread_id", c.Params.ThreadId) + auditRec.AddMeta("team_id", c.Params.TeamId) + auditRec.AddMeta("post_id", c.Params.PostId) + + if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) { + c.SetPermissionError(model.PermissionEditOtherUsers) + return + } + + if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) { + c.SetPermissionError(model.PermissionReadChannel) + return + } + + thread, err := c.App.UpdateThreadReadForUserByPost(c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.PostId) + if err != nil { + c.Err = err + return + } + + if err := json.NewEncoder(w).Encode(thread); err != nil { + mlog.Warn("Error while writing response", mlog.Err(err)) + } + + auditRec.Success() +} + func unfollowThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) { c.RequireUserId().RequireThreadId().RequireTeamId() if c.Err != nil { diff --git a/api4/user_test.go b/api4/user_test.go index fc5affd365..0afab99d12 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -5998,7 +5998,7 @@ func TestThreadSocketEvents(t *testing.T) { require.Truef(t, caught, "User should have received %s event", model.WebsocketEventThreadReadChanged) }) - _, resp, err = th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, rpost.CreateAt) + _, resp, err = th.Client.SetThreadUnreadByPostId(th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, rpost.Id) require.NoError(t, err) CheckOKStatus(t, resp) @@ -6012,7 +6012,7 @@ func TestThreadSocketEvents(t *testing.T) { caught = true data := ev.GetData() - require.EqualValues(t, rpost.CreateAt, data["timestamp"]) + require.EqualValues(t, rpost.CreateAt-1, data["timestamp"]) require.EqualValues(t, float64(0), data["previous_unread_replies"]) require.EqualValues(t, float64(0), data["previous_unread_mentions"]) require.EqualValues(t, float64(1), data["unread_replies"]) @@ -6473,7 +6473,7 @@ func TestReadThreads(t *testing.T) { require.Greater(t, uss2.Threads[0].LastViewedAt, uss.Threads[0].LastViewedAt) }) - t.Run("1 thread", func(t *testing.T) { + t.Run("1 thread by timestamp", func(t *testing.T) { defer th.App.Srv().Store.Post().PermanentDeleteByUser(th.BasicUser.Id) defer th.App.Srv().Store.Post().PermanentDeleteByUser(th.SystemAdminUser.Id) @@ -6500,6 +6500,42 @@ func TestReadThreads(t *testing.T) { uss3, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 2, nil) require.Equal(t, uss3.Threads[0].LastViewedAt, timestamp) }) + + t.Run("1 thread by post id", func(t *testing.T) { + defer th.App.Srv().Store.Post().PermanentDeleteByUser(th.BasicUser.Id) + defer th.App.Srv().Store.Post().PermanentDeleteByUser(th.SystemAdminUser.Id) + + rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsgC1"}) + reply1, _ := postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReplyC1", RootId: rpost.Id}) + reply2, _ := postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReplyC1", RootId: rpost.Id}) + reply3, _ := postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReplyC1", RootId: rpost.Id}) + + checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 3, 1, nil) + + _, resp, err := th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, reply3.CreateAt+1) + require.NoError(t, err) + CheckOKStatus(t, resp) + + checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 0, 1, nil) + + _, resp, err = th.Client.SetThreadUnreadByPostId(th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, reply1.Id) + require.NoError(t, err) + CheckOKStatus(t, resp) + + checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 3, 1, nil) + + _, resp, err = th.Client.SetThreadUnreadByPostId(th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, reply2.Id) + require.NoError(t, err) + CheckOKStatus(t, resp) + + checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 1, nil) + + _, resp, err = th.Client.SetThreadUnreadByPostId(th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, reply3.Id) + require.NoError(t, err) + CheckOKStatus(t, resp) + + checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 1, nil) + }) } func TestMarkThreadUnreadMentionCount(t *testing.T) { diff --git a/app/app_iface.go b/app/app_iface.go index dde3b80d1f..b281cf9ced 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -1077,6 +1077,7 @@ type AppIface interface { UpdateThreadFollowForUser(userID, teamID, threadID string, state bool) *model.AppError UpdateThreadFollowForUserFromChannelAdd(userID, teamID, threadID string) *model.AppError UpdateThreadReadForUser(currentSessionId, userID, teamID, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) + UpdateThreadReadForUserByPost(currentSessionId, userID, teamID, threadID, postID string) (*model.ThreadResponse, *model.AppError) UpdateThreadsReadForUser(userID, teamID string) *model.AppError UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError) UpdateUserActive(c *request.Context, userID string, active bool) *model.AppError diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 14f3de61e6..ea2898e482 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -16964,6 +16964,28 @@ func (a *OpenTracingAppLayer) UpdateThreadReadForUser(currentSessionId string, u return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) UpdateThreadReadForUserByPost(currentSessionId string, userID string, teamID string, threadID string, postID string) (*model.ThreadResponse, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadReadForUserByPost") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.UpdateThreadReadForUserByPost(currentSessionId, userID, teamID, threadID, postID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) UpdateThreadsReadForUser(userID string, teamID string) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadsReadForUser") diff --git a/app/user.go b/app/user.go index fb2883bc61..c88ab9c7de 100644 --- a/app/user.go +++ b/app/user.go @@ -2476,6 +2476,19 @@ func (a *App) UpdateThreadFollowForUserFromChannelAdd(userID, teamID, threadID s return nil } +func (a *App) UpdateThreadReadForUserByPost(currentSessionId, userID, teamID, threadID, postID string) (*model.ThreadResponse, *model.AppError) { + post, err := a.GetSinglePost(postID) + if err != nil { + return nil, err + } + + if post.RootId != threadID && postID != threadID { + return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user_by_post.app_error", nil, "", http.StatusBadRequest) + } + + return a.UpdateThreadReadForUser(currentSessionId, userID, teamID, threadID, post.CreateAt-1) +} + func (a *App) UpdateThreadReadForUser(currentSessionId, userID, teamID, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) { user, err := a.GetUser(userID) if err != nil { diff --git a/i18n/en.json b/i18n/en.json index 15015fedf9..1ae8f0b2dd 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6495,6 +6495,10 @@ "id": "app.user.update_thread_read_for_user.app_error", "translation": "Unable to update read state for thread" }, + { + "id": "app.user.update_thread_read_for_user_by_post.app_error", + "translation": "Invalid post_id" + }, { "id": "app.user.update_threads_read_for_user.app_error", "translation": "Unable to update all user threads as read" diff --git a/model/client4.go b/model/client4.go index 2f123b54a0..c6e7d887f1 100644 --- a/model/client4.go +++ b/model/client4.go @@ -7920,6 +7920,18 @@ func (c *Client4) UpdateThreadsReadForUser(userId, teamId string) (*Response, er return BuildResponse(r), nil } +func (c *Client4) SetThreadUnreadByPostId(userId, teamId, threadId, postId string) (*ThreadResponse, *Response, error) { + r, err := c.DoAPIPost(fmt.Sprintf("%s/set_unread/%s", c.userThreadRoute(userId, teamId, threadId), postId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var thread ThreadResponse + json.NewDecoder(r.Body).Decode(&thread) + + return &thread, BuildResponse(r), nil +} + func (c *Client4) UpdateThreadReadForUser(userId, teamId, threadId string, timestamp int64) (*ThreadResponse, *Response, error) { r, err := c.DoAPIPut(fmt.Sprintf("%s/read/%d", c.userThreadRoute(userId, teamId, threadId), timestamp), "") if err != nil {