[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
Этот коммит содержится в:
37
api4/user.go
37
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(followThreadByUser)).Methods("PUT")
|
||||||
api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(unfollowThreadByUser)).Methods("DELETE")
|
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("/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) {
|
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()
|
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) {
|
func unfollowThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
c.RequireUserId().RequireThreadId().RequireTeamId()
|
c.RequireUserId().RequireThreadId().RequireTeamId()
|
||||||
if c.Err != nil {
|
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)
|
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)
|
require.NoError(t, err)
|
||||||
CheckOKStatus(t, resp)
|
CheckOKStatus(t, resp)
|
||||||
|
|
||||||
@@ -6012,7 +6012,7 @@ func TestThreadSocketEvents(t *testing.T) {
|
|||||||
caught = true
|
caught = true
|
||||||
|
|
||||||
data := ev.GetData()
|
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_replies"])
|
||||||
require.EqualValues(t, float64(0), data["previous_unread_mentions"])
|
require.EqualValues(t, float64(0), data["previous_unread_mentions"])
|
||||||
require.EqualValues(t, float64(1), data["unread_replies"])
|
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)
|
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.BasicUser.Id)
|
||||||
defer th.App.Srv().Store.Post().PermanentDeleteByUser(th.SystemAdminUser.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)
|
uss3, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 2, nil)
|
||||||
require.Equal(t, uss3.Threads[0].LastViewedAt, timestamp)
|
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) {
|
func TestMarkThreadUnreadMentionCount(t *testing.T) {
|
||||||
|
|||||||
@@ -1077,6 +1077,7 @@ type AppIface interface {
|
|||||||
UpdateThreadFollowForUser(userID, teamID, threadID string, state bool) *model.AppError
|
UpdateThreadFollowForUser(userID, teamID, threadID string, state bool) *model.AppError
|
||||||
UpdateThreadFollowForUserFromChannelAdd(userID, teamID, threadID string) *model.AppError
|
UpdateThreadFollowForUserFromChannelAdd(userID, teamID, threadID string) *model.AppError
|
||||||
UpdateThreadReadForUser(currentSessionId, userID, teamID, threadID string, timestamp int64) (*model.ThreadResponse, *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
|
UpdateThreadsReadForUser(userID, teamID string) *model.AppError
|
||||||
UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError)
|
UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError)
|
||||||
UpdateUserActive(c *request.Context, userID string, active bool) *model.AppError
|
UpdateUserActive(c *request.Context, userID string, active bool) *model.AppError
|
||||||
|
|||||||
@@ -16964,6 +16964,28 @@ func (a *OpenTracingAppLayer) UpdateThreadReadForUser(currentSessionId string, u
|
|||||||
return resultVar0, resultVar1
|
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 {
|
func (a *OpenTracingAppLayer) UpdateThreadsReadForUser(userID string, teamID string) *model.AppError {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadsReadForUser")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadsReadForUser")
|
||||||
|
|||||||
13
app/user.go
13
app/user.go
@@ -2476,6 +2476,19 @@ func (a *App) UpdateThreadFollowForUserFromChannelAdd(userID, teamID, threadID s
|
|||||||
return nil
|
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) {
|
func (a *App) UpdateThreadReadForUser(currentSessionId, userID, teamID, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) {
|
||||||
user, err := a.GetUser(userID)
|
user, err := a.GetUser(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6495,6 +6495,10 @@
|
|||||||
"id": "app.user.update_thread_read_for_user.app_error",
|
"id": "app.user.update_thread_read_for_user.app_error",
|
||||||
"translation": "Unable to update read state for thread"
|
"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",
|
"id": "app.user.update_threads_read_for_user.app_error",
|
||||||
"translation": "Unable to update all user threads as read"
|
"translation": "Unable to update all user threads as read"
|
||||||
|
|||||||
@@ -7920,6 +7920,18 @@ func (c *Client4) UpdateThreadsReadForUser(userId, teamId string) (*Response, er
|
|||||||
return BuildResponse(r), nil
|
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) {
|
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), "")
|
r, err := c.DoAPIPut(fmt.Sprintf("%s/read/%d", c.userThreadRoute(userId, teamId, threadId), timestamp), "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user