From 23d51ed1f298c0ab300842db14a574fe6a84b788 Mon Sep 17 00:00:00 2001 From: Eli Yukelzon Date: Tue, 2 Mar 2021 16:49:00 +0200 Subject: [PATCH] MM-32655 - Collapsed threads websocket handling (#16909) Co-authored-by: Mattermod --- api4/user.go | 8 ++--- api4/user_test.go | 19 +++++++----- app/app_iface.go | 4 +-- app/channel.go | 23 +++++++++++++-- app/notification.go | 16 +++++----- app/opentracing/opentracing_layer.go | 14 ++++----- app/post.go | 6 ++-- app/user.go | 44 ++++++++++++++++------------ model/client4.go | 8 +++-- model/preference.go | 17 +++++------ model/thread.go | 6 ++++ 11 files changed, 102 insertions(+), 63 deletions(-) diff --git a/api4/user.go b/api4/user.go index 6cf878b7c1..3490e0fd75 100644 --- a/api4/user.go +++ b/api4/user.go @@ -2945,13 +2945,13 @@ func updateReadStateThreadByUser(c *Context, w http.ResponseWriter, r *http.Requ return } - err := c.App.UpdateThreadReadForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.Timestamp) + thread, err := c.App.UpdateThreadReadForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.Timestamp) if err != nil { c.Err = err return } - ReturnStatusOK(w) + w.Write([]byte(thread.ToJson())) auditRec.Success() } @@ -2973,7 +2973,7 @@ func unfollowThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) { return } - err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.ThreadId, false) + err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, false) if err != nil { c.Err = err return @@ -3001,7 +3001,7 @@ func followThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) { return } - err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.ThreadId, true) + err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, true) if err != nil { c.Err = err return diff --git a/api4/user_test.go b/api4/user_test.go index 27f2977eef..497f0cc500 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -5475,10 +5475,13 @@ func TestThreadSocketEvents(t *testing.T) { case ev := <-userWSClient.EventChannel: if ev.EventType() == model.WEBSOCKET_EVENT_THREAD_UPDATED { caught = true - thread, err := model.ThreadFromJson(ev.GetData()["thread"].(string)) + thread, err := model.ThreadResponseFromJson(ev.GetData()["thread"].(string)) require.NoError(t, err) - require.Contains(t, thread.Participants, th.BasicUser.Id) - require.Contains(t, thread.Participants, th.BasicUser2.Id) + for _, p := range thread.Participants { + if p.Id != th.BasicUser.Id && p.Id != th.BasicUser2.Id { + require.Fail(t, "invalid participants") + } + } } case <-time.After(1 * time.Second): return @@ -5510,7 +5513,7 @@ func TestThreadSocketEvents(t *testing.T) { require.Truef(t, caught, "User should have received %s event", model.WEBSOCKET_EVENT_THREAD_FOLLOW_CHANGED) }) - resp = th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, 123) + _, resp = th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, 123) CheckNoError(t, resp) CheckOKStatus(t, resp) @@ -5650,7 +5653,7 @@ func TestMaintainUnreadRepliesInThread(t *testing.T) { checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 0, 1, nil) // mark other user's read state - resp = th.SystemAdminClient.UpdateThreadReadForUser(th.SystemAdminUser.Id, th.BasicTeam.Id, rpost.Id, model.GetMillis()) + _, resp = th.SystemAdminClient.UpdateThreadReadForUser(th.SystemAdminUser.Id, th.BasicTeam.Id, rpost.Id, model.GetMillis()) CheckNoError(t, resp) CheckOKStatus(t, resp) @@ -5658,7 +5661,7 @@ func TestMaintainUnreadRepliesInThread(t *testing.T) { checkThreadListReplies(t, th, th.SystemAdminClient, th.SystemAdminUser.Id, 0, 0, &model.GetUserThreadsOpts{Unread: true}) // restore unread to an old date - resp = th.SystemAdminClient.UpdateThreadReadForUser(th.SystemAdminUser.Id, th.BasicTeam.Id, rpost.Id, 123) + _, resp = th.SystemAdminClient.UpdateThreadReadForUser(th.SystemAdminUser.Id, th.BasicTeam.Id, rpost.Id, 123) CheckNoError(t, resp) CheckOKStatus(t, resp) @@ -5876,7 +5879,7 @@ func TestReadThreads(t *testing.T) { uss, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 2, nil) - resp := th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, rrpost.Id, model.GetMillis()+10) + _, resp := th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, rrpost.Id, model.GetMillis()+10) CheckNoError(t, resp) CheckOKStatus(t, resp) @@ -5884,7 +5887,7 @@ func TestReadThreads(t *testing.T) { require.Greater(t, uss2.Threads[0].LastViewedAt, uss.Threads[0].LastViewedAt) timestamp := model.GetMillis() - resp = th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, rrpost.Id, timestamp) + _, resp = th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, rrpost.Id, timestamp) CheckNoError(t, resp) CheckOKStatus(t, resp) diff --git a/app/app_iface.go b/app/app_iface.go index 49f525152b..5ae41d504f 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -1020,8 +1020,8 @@ type AppIface interface { UpdateTeamMemberSchemeRoles(teamID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.TeamMember, *model.AppError) UpdateTeamPrivacy(teamID string, teamType string, allowOpenInvite bool) *model.AppError UpdateTeamScheme(team *model.Team) (*model.Team, *model.AppError) - UpdateThreadFollowForUser(userID, threadId string, state bool) *model.AppError - UpdateThreadReadForUser(userID, teamID, threadId string, timestamp int64) *model.AppError + UpdateThreadFollowForUser(userID, teamID, threadID string, state bool) *model.AppError + UpdateThreadReadForUser(userID, teamID, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) UpdateThreadsReadForUser(userID, teamID string) *model.AppError UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError) UpdateUserActive(userID string, active bool) *model.AppError diff --git a/app/channel.go b/app/channel.go index 746349c542..5d057108bd 100644 --- a/app/channel.go +++ b/app/channel.go @@ -2371,8 +2371,13 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string) (*model. return nil, err } - if *a.Config().ServiceSettings.ThreadAutoFollow && post.RootId != "" { - threadMembership, _ := a.Srv().Store.Thread().GetMembershipForUser(user.Id, post.RootId) + if *a.Config().ServiceSettings.ThreadAutoFollow { + threadId := post.RootId + if post.RootId == "" { + threadId = post.Id + } + + threadMembership, _ := a.Srv().Store.Thread().GetMembershipForUser(user.Id, threadId) if threadMembership != nil { channel, nErr := a.Srv().Store.Channel().Get(post.ChannelId, true) if nErr != nil { @@ -2386,6 +2391,20 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string) (*model. if nErr != nil { return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError) } + thread, _ := a.Srv().Store.Thread().GetThreadForUser(userID, channel.TeamId, threadId, true) + a.sanitizeProfiles(thread.Participants, false) + thread.Post.SanitizeProps() + + payload := thread.ToJson() + sendEvent := *a.Config().ServiceSettings.CollapsedThreads == model.COLLAPSED_THREADS_DEFAULT_ON + if preference, err := a.Srv().Store.Preference().Get(userID, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_COLLAPSED_THREADS_ENABLED); err == nil { + sendEvent = preference.Value == "on" + } + if sendEvent { + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_UPDATED, channel.TeamId, "", userID, nil) + message.Add("thread", payload) + a.Publish(message) + } } } diff --git a/app/notification.go b/app/notification.go index f179b06138..c776eaa953 100644 --- a/app/notification.go +++ b/app/notification.go @@ -7,7 +7,6 @@ import ( "context" "net/http" "sort" - "strconv" "strings" "unicode" "unicode/utf8" @@ -168,7 +167,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod } } - mentionedUsersList := make([]string, 0, len(mentions.Mentions)) + mentionedUsersList := make(model.StringArray, 0, len(mentions.Mentions)) updateMentionChans := []chan *model.AppError{} mentionAutofollowChans := []chan *model.AppError{} threadParticipants := map[string]bool{post.UserId: true} @@ -433,16 +432,19 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod if err != nil { return nil, errors.Wrapf(err, "cannot get thread %q", post.RootId) } - payload := thread.ToJson() for _, uid := range thread.Participants { sendEvent := *a.Config().ServiceSettings.CollapsedThreads == model.COLLAPSED_THREADS_DEFAULT_ON // check if a participant has overridden collapsed threads settings - if preference, err := a.Srv().Store.Preference().Get(uid, model.PREFERENCE_CATEGORY_COLLAPSED_THREADS_SETTINGS, model.PREFERENCE_NAME_COLLAPSED_THREADS_ENABLED); err == nil { - sendEvent, _ = strconv.ParseBool(preference.Value) + if preference, err := a.Srv().Store.Preference().Get(uid, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_COLLAPSED_THREADS_ENABLED); err == nil { + sendEvent = preference.Value == "on" } if sendEvent { - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_UPDATED, "", "", uid, nil) - message.Add("thread", payload) + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_UPDATED, team.Id, "", uid, nil) + userThread, _ := a.Srv().Store.Thread().GetThreadForUser(uid, channel.TeamId, thread.PostId, true) + a.sanitizeProfiles(userThread.Participants, false) + userThread.Post.SanitizeProps() + message.Add("thread", userThread.ToJson()) a.Publish(message) } } diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 17c49a15b3..a988431b48 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -15643,7 +15643,7 @@ func (a *OpenTracingAppLayer) UpdateTeamScheme(team *model.Team) (*model.Team, * return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) UpdateThreadFollowForUser(userID string, threadId string, state bool) *model.AppError { +func (a *OpenTracingAppLayer) UpdateThreadFollowForUser(userID string, teamID string, threadID string, state bool) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadFollowForUser") @@ -15655,7 +15655,7 @@ func (a *OpenTracingAppLayer) UpdateThreadFollowForUser(userID string, threadId }() defer span.Finish() - resultVar0 := a.app.UpdateThreadFollowForUser(userID, threadId, state) + resultVar0 := a.app.UpdateThreadFollowForUser(userID, teamID, threadID, state) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) @@ -15665,7 +15665,7 @@ func (a *OpenTracingAppLayer) UpdateThreadFollowForUser(userID string, threadId return resultVar0 } -func (a *OpenTracingAppLayer) UpdateThreadReadForUser(userID string, teamID string, threadId string, timestamp int64) *model.AppError { +func (a *OpenTracingAppLayer) UpdateThreadReadForUser(userID string, teamID string, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadReadForUser") @@ -15677,14 +15677,14 @@ func (a *OpenTracingAppLayer) UpdateThreadReadForUser(userID string, teamID stri }() defer span.Finish() - resultVar0 := a.app.UpdateThreadReadForUser(userID, teamID, threadId, timestamp) + resultVar0, resultVar1 := a.app.UpdateThreadReadForUser(userID, teamID, threadID, timestamp) - if resultVar0 != nil { - span.LogFields(spanlog.Error(resultVar0)) + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) ext.Error.Set(span, true) } - return resultVar0 + return resultVar0, resultVar1 } func (a *OpenTracingAppLayer) UpdateThreadsReadForUser(userID string, teamID string) *model.AppError { diff --git a/app/post.go b/app/post.go index 4a171be1f9..c665d1054b 100644 --- a/app/post.go +++ b/app/post.go @@ -1390,8 +1390,10 @@ func (a *App) countThreadMentions(user *model.User, post *model.Post, teamID str } mentions := getExplicitMentions(post, keywords, groups) - if _, ok := mentions.Mentions[user.Id]; ok { - count += 1 + if post.UpdateAt >= timestamp { + if _, ok := mentions.Mentions[user.Id]; ok { + count += 1 + } } for _, p := range posts { diff --git a/app/user.go b/app/user.go index 77c6f920cb..d463716a67 100644 --- a/app/user.go +++ b/app/user.go @@ -2397,54 +2397,60 @@ func (a *App) UpdateThreadsReadForUser(userID, teamID string) *model.AppError { if nErr != nil { return model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError) } - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_READ_CHANGED, "", "", userID, nil) + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_READ_CHANGED, teamID, "", userID, nil) a.Publish(message) return nil } -func (a *App) UpdateThreadFollowForUser(userID, threadId string, state bool) *model.AppError { - err := a.Srv().Store.Thread().CreateMembershipIfNeeded(userID, threadId, state, false, true) +func (a *App) UpdateThreadFollowForUser(userID, teamID, threadID string, state bool) *model.AppError { + err := a.Srv().Store.Thread().CreateMembershipIfNeeded(userID, threadID, state, false, true) if err != nil { return model.NewAppError("UpdateThreadFollowForUser", "app.user.update_thread_follow_for_user.app_error", nil, err.Error(), http.StatusInternalServerError) } - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_FOLLOW_CHANGED, "", "", userID, nil) - message.Add("thread_id", threadId) + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_FOLLOW_CHANGED, teamID, "", userID, nil) + message.Add("thread_id", threadID) message.Add("state", state) a.Publish(message) return nil } -func (a *App) UpdateThreadReadForUser(userID, teamID, threadId string, timestamp int64) *model.AppError { +func (a *App) UpdateThreadReadForUser(userID, teamID, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) { user, err := a.GetUser(userID) if err != nil { - return err + return nil, err } - membership, nErr := a.Srv().Store.Thread().GetMembershipForUser(userID, threadId) + membership, nErr := a.Srv().Store.Thread().GetMembershipForUser(userID, threadID) if nErr != nil { - return model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError) + return nil, model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError) } - post, err := a.GetSinglePost(threadId) + post, err := a.GetSinglePost(threadID) if err != nil { - return err + return nil, err } - membership.UnreadMentions, err = a.countThreadMentions(user, post, teamID, timestamp) if err != nil { - return err + return nil, err } membership.Following = true _, nErr = a.Srv().Store.Thread().UpdateMembership(membership) if nErr != nil { - return model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError) + return nil, model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError) } - nErr = a.Srv().Store.Thread().MarkAsRead(userID, threadId, timestamp) + nErr = a.Srv().Store.Thread().MarkAsRead(userID, threadID, timestamp) if nErr != nil { - return model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError) + return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError) } - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_READ_CHANGED, "", "", userID, nil) - message.Add("thread_id", threadId) + thread, err := a.GetThreadForUser(userID, teamID, threadID, false) + if err != nil { + return nil, err + } + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_READ_CHANGED, teamID, "", userID, nil) + message.Add("thread_id", threadID) message.Add("timestamp", timestamp) + message.Add("unread_mentions", membership.UnreadMentions) + message.Add("unread_replies", thread.UnreadReplies) + message.Add("channel_id", post.ChannelId) a.Publish(message) - return nil + return thread, nil } diff --git a/model/client4.go b/model/client4.go index 758c268469..809fa8fbb7 100644 --- a/model/client4.go +++ b/model/client4.go @@ -5966,14 +5966,16 @@ func (c *Client4) UpdateThreadsReadForUser(userId, teamId string) *Response { return BuildResponse(r) } -func (c *Client4) UpdateThreadReadForUser(userId, teamId, threadId string, timestamp int64) *Response { +func (c *Client4) UpdateThreadReadForUser(userId, teamId, threadId string, timestamp int64) (*ThreadResponse, *Response) { r, appErr := c.DoApiPut(fmt.Sprintf("%s/read/%d", c.GetUserThreadRoute(userId, teamId, threadId), timestamp), "") if appErr != nil { - return BuildErrorResponse(r, appErr) + return nil, BuildErrorResponse(r, appErr) } defer closeBody(r) + var thread ThreadResponse + json.NewDecoder(r.Body).Decode(&thread) - return BuildResponse(r) + return &thread, BuildResponse(r) } func (c *Client4) UpdateThreadFollowForUser(userId, teamId, threadId string, state bool) *Response { diff --git a/model/preference.go b/model/preference.go index 5559cc0ae6..9ba9b60b68 100644 --- a/model/preference.go +++ b/model/preference.go @@ -13,17 +13,16 @@ import ( ) const ( - PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show" - PREFERENCE_CATEGORY_GROUP_CHANNEL_SHOW = "group_channel_show" - PREFERENCE_CATEGORY_TUTORIAL_STEPS = "tutorial_step" - PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings" - PREFERENCE_CATEGORY_FLAGGED_POST = "flagged_post" - PREFERENCE_CATEGORY_FAVORITE_CHANNEL = "favorite_channel" - PREFERENCE_CATEGORY_SIDEBAR_SETTINGS = "sidebar_settings" - PREFERENCE_CATEGORY_COLLAPSED_THREADS_SETTINGS = "collapsed_threads_settings" + PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show" + PREFERENCE_CATEGORY_GROUP_CHANNEL_SHOW = "group_channel_show" + PREFERENCE_CATEGORY_TUTORIAL_STEPS = "tutorial_step" + PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings" + PREFERENCE_CATEGORY_FLAGGED_POST = "flagged_post" + PREFERENCE_CATEGORY_FAVORITE_CHANNEL = "favorite_channel" + PREFERENCE_CATEGORY_SIDEBAR_SETTINGS = "sidebar_settings" PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings" - PREFERENCE_NAME_COLLAPSED_THREADS_ENABLED = "collapsed_threads_enabled" + PREFERENCE_NAME_COLLAPSED_THREADS_ENABLED = "collapsed_reply_threads" PREFERENCE_NAME_CHANNEL_DISPLAY_MODE = "channel_display_mode" PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews" PREFERENCE_NAME_MESSAGE_DISPLAY = "message_display" diff --git a/model/thread.go b/model/thread.go index fb7ca9f991..9a6b5fab64 100644 --- a/model/thread.go +++ b/model/thread.go @@ -61,6 +61,12 @@ func (o *ThreadResponse) ToJson() string { return string(b) } +func ThreadResponseFromJson(s string) (*ThreadResponse, error) { + var t ThreadResponse + err := json.Unmarshal([]byte(s), &t) + return &t, err +} + func (o *Threads) ToJson() string { b, _ := json.Marshal(o) return string(b)