MM-32655 - Collapsed threads websocket handling (#16909)

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Eli Yukelzon
2021-03-02 16:49:00 +02:00
committed by GitHub
parent 78355ae2a7
commit 23d51ed1f2
11 changed files with 102 additions and 63 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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