[MM-41185]: new API to mark thread as unread (#19951)

* [MM-41185]: new API to mark thread as unread

Until now we mark a thread as read/unread by sending a timestamp to the
server. This created some issues when marking a thread as unread from a
post. We needed to send the post.create_at - 1 as a timestamp but we
didn't do so consistently.

This commit adds a new API to set a thread as unread by post id.
Making all clients agnostic of the timestamp, and thus solving consistency issues.

Endpoint: /api/v4/users/{user_id}/teams/{team_id}/threads/{thread_id}/set_unread/{post_id}

* Updates client.go adding SetThreadUnreadByPostId

* Guards endpoint behind read channel permission

* Returns status 400 if post_id not belong in thread

* Root post as post_id should be permitted
Этот коммит содержится в:
Kyriakos Z
2022-04-15 10:55:47 +03:00
коммит произвёл GitHub
родитель 6ab7cd0f1a
Коммит 79e36d4596
7 изменённых файлов: 128 добавлений и 3 удалений

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

@@ -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 {

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

@@ -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) {