MM-34758 Collapsed Reply Threads without mobile support (#17424)

Summary
added support for legacy clients accessing server
added collapsed_threads_supported param to viewChannel API and setPostUnread API

Ticket Link
https://mattermost.atlassian.net/browse/MM-34758

Related Webapp PR
mattermost/mattermost-webapp#7933
Этот коммит содержится в:
Eli Yukelzon
2021-05-26 18:10:25 +03:00
коммит произвёл GitHub
родитель ebed0c67f7
Коммит 46649292f8
21 изменённых файлов: 417 добавлений и 65 удалений

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

@@ -1330,7 +1330,7 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
times, err := c.App.ViewChannel(view, c.Params.UserId, c.AppContext.Session().Id) times, err := c.App.ViewChannel(view, c.Params.UserId, c.AppContext.Session().Id, view.CollapsedThreadsSupported)
if err != nil { if err != nil {
c.Err = err c.Err = err
return return

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

@@ -7,6 +7,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"os"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -4178,6 +4179,7 @@ func TestMoveChannel(t *testing.T) {
func TestRootMentionsCount(t *testing.T) { func TestRootMentionsCount(t *testing.T) {
th := Setup(t).InitBasic() th := Setup(t).InitBasic()
defer th.TearDown() defer th.TearDown()
Client := th.Client Client := th.Client
user := th.BasicUser user := th.BasicUser
channel := th.BasicChannel channel := th.BasicChannel
@@ -4214,3 +4216,44 @@ func TestRootMentionsCount(t *testing.T) {
require.Equal(t, int64(1), counts.MentionCountRoot) require.Equal(t, int64(1), counts.MentionCountRoot)
require.Equal(t, int64(2), counts.MentionCount) require.Equal(t, int64(2), counts.MentionCount)
} }
func TestViewChannelWithoutCollapsedThreads(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS")
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.COLLAPSED_THREADS_DEFAULT_ON
})
Client := th.Client
user := th.BasicUser
team := th.BasicTeam
channel := th.BasicChannel
// mention the user in a root post
post1, resp := th.SystemAdminClient.CreatePost(&model.Post{ChannelId: channel.Id, Message: "hey @" + user.Username})
CheckNoError(t, resp)
// mention the user in a reply post
post2 := &model.Post{ChannelId: channel.Id, Message: "reply at @" + user.Username, RootId: post1.Id}
_, resp = th.SystemAdminClient.CreatePost(post2)
CheckNoError(t, resp)
threads, resp := Client.GetUserThreads(user.Id, team.Id, model.GetUserThreadsOpts{})
CheckNoError(t, resp)
require.EqualValues(t, int64(1), threads.TotalUnreadMentions)
// simulate opening the channel from an old client
_, resp = Client.ViewChannel(user.Id, &model.ChannelView{
ChannelId: channel.Id,
PrevChannelId: "",
CollapsedThreadsSupported: false,
})
CheckNoError(t, resp)
threads, resp = Client.GetUserThreads(user.Id, team.Id, model.GetUserThreadsOpts{})
CheckNoError(t, resp)
require.Zero(t, threads.TotalUnreadMentions)
}

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

@@ -639,11 +639,15 @@ func patchPost(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(patchedPost.ToJson())) w.Write([]byte(patchedPost.ToJson()))
} }
func setPostUnread(c *Context, w http.ResponseWriter, _ *http.Request) { func setPostUnread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId().RequireUserId() c.RequirePostId().RequireUserId()
if c.Err != nil { if c.Err != nil {
return return
} }
props := model.MapBoolFromJson(r.Body)
collapsedThreadsSupported := props["collapsed_threads_supported"]
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) { if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS)
return return
@@ -653,7 +657,7 @@ func setPostUnread(c *Context, w http.ResponseWriter, _ *http.Request) {
return return
} }
state, err := c.App.MarkChannelAsUnreadFromPost(c.Params.PostId, c.Params.UserId) state, err := c.App.MarkChannelAsUnreadFromPost(c.Params.PostId, c.Params.UserId, collapsedThreadsSupported)
if err != nil { if err != nil {
c.Err = err c.Err = err
return return

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

@@ -2493,14 +2493,14 @@ func TestSetChannelUnread(t *testing.T) {
unread, err = th.App.GetChannelUnread(c1.Id, u2.Id) unread, err = th.App.GetChannelUnread(c1.Id, u2.Id)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, int64(4), unread.MsgCount) require.Equal(t, int64(4), unread.MsgCount)
_, err = th.App.ViewChannel(c1toc2, u2.Id, s2.Id) _, err = th.App.ViewChannel(c1toc2, u2.Id, s2.Id, false)
require.Nil(t, err) require.Nil(t, err)
unread, err = th.App.GetChannelUnread(c1.Id, u2.Id) unread, err = th.App.GetChannelUnread(c1.Id, u2.Id)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, int64(0), unread.MsgCount) require.Equal(t, int64(0), unread.MsgCount)
t.Run("Unread last one", func(t *testing.T) { t.Run("Unread last one", func(t *testing.T) {
r := th.Client.SetPostUnread(u1.Id, p2.Id) r := th.Client.SetPostUnread(u1.Id, p2.Id, true)
checkHTTPStatus(t, r, 200, false) checkHTTPStatus(t, r, 200, false)
unread, err := th.App.GetChannelUnread(c1.Id, u1.Id) unread, err := th.App.GetChannelUnread(c1.Id, u1.Id)
require.Nil(t, err) require.Nil(t, err)
@@ -2508,12 +2508,12 @@ func TestSetChannelUnread(t *testing.T) {
}) })
t.Run("Unread on a private channel", func(t *testing.T) { t.Run("Unread on a private channel", func(t *testing.T) {
r := th.Client.SetPostUnread(u1.Id, pp2.Id) r := th.Client.SetPostUnread(u1.Id, pp2.Id, true)
assert.Equal(t, 200, r.StatusCode) assert.Equal(t, 200, r.StatusCode)
unread, err := th.App.GetChannelUnread(th.BasicPrivateChannel.Id, u1.Id) unread, err := th.App.GetChannelUnread(th.BasicPrivateChannel.Id, u1.Id)
require.Nil(t, err) require.Nil(t, err)
assert.Equal(t, int64(1), unread.MsgCount) assert.Equal(t, int64(1), unread.MsgCount)
r = th.Client.SetPostUnread(u1.Id, pp1.Id) r = th.Client.SetPostUnread(u1.Id, pp1.Id, true)
assert.Equal(t, 200, r.StatusCode) assert.Equal(t, 200, r.StatusCode)
unread, err = th.App.GetChannelUnread(th.BasicPrivateChannel.Id, u1.Id) unread, err = th.App.GetChannelUnread(th.BasicPrivateChannel.Id, u1.Id)
require.Nil(t, err) require.Nil(t, err)
@@ -2521,7 +2521,7 @@ func TestSetChannelUnread(t *testing.T) {
}) })
t.Run("Can't unread an imaginary post", func(t *testing.T) { t.Run("Can't unread an imaginary post", func(t *testing.T) {
r := th.Client.SetPostUnread(u1.Id, "invalid4ofngungryquinj976y") r := th.Client.SetPostUnread(u1.Id, "invalid4ofngungryquinj976y", true)
assert.Equal(t, http.StatusForbidden, r.StatusCode) assert.Equal(t, http.StatusForbidden, r.StatusCode)
}) })
@@ -2531,18 +2531,18 @@ func TestSetChannelUnread(t *testing.T) {
c3.Login(u3.Email, u3.Password) c3.Login(u3.Email, u3.Password)
t.Run("Can't unread channels you don't belong to", func(t *testing.T) { t.Run("Can't unread channels you don't belong to", func(t *testing.T) {
r := c3.SetPostUnread(u3.Id, pp1.Id) r := c3.SetPostUnread(u3.Id, pp1.Id, true)
assert.Equal(t, http.StatusForbidden, r.StatusCode) assert.Equal(t, http.StatusForbidden, r.StatusCode)
}) })
t.Run("Can't unread users you don't have permission to edit", func(t *testing.T) { t.Run("Can't unread users you don't have permission to edit", func(t *testing.T) {
r := c3.SetPostUnread(u1.Id, pp1.Id) r := c3.SetPostUnread(u1.Id, pp1.Id, true)
assert.Equal(t, http.StatusForbidden, r.StatusCode) assert.Equal(t, http.StatusForbidden, r.StatusCode)
}) })
t.Run("Can't unread if user is not logged in", func(t *testing.T) { t.Run("Can't unread if user is not logged in", func(t *testing.T) {
th.Client.Logout() th.Client.Logout()
response := th.Client.SetPostUnread(u1.Id, p2.Id) response := th.Client.SetPostUnread(u1.Id, p2.Id, true)
checkHTTPStatus(t, response, http.StatusUnauthorized, true) checkHTTPStatus(t, response, http.StatusUnauthorized, true)
}) })
} }
@@ -2565,7 +2565,7 @@ func TestMarkUnreadCausesAutofollow(t *testing.T) {
require.Nil(t, appErr) require.Nil(t, appErr)
require.Zero(t, threads.Total) require.Zero(t, threads.Total)
_, appErr = th.App.MarkChannelAsUnreadFromPost(replyPost.Id, th.BasicUser.Id) _, appErr = th.App.MarkChannelAsUnreadFromPost(replyPost.Id, th.BasicUser.Id, true)
require.Nil(t, appErr) require.Nil(t, appErr)
threads, appErr = th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{}) threads, appErr = th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
@@ -2573,3 +2573,70 @@ func TestMarkUnreadCausesAutofollow(t *testing.T) {
require.NotZero(t, threads.Total) require.NotZero(t, threads.Total)
} }
func TestSetPostUnreadWithoutCollapsedThreads(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS")
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.COLLAPSED_THREADS_DEFAULT_ON
})
// user2: first root mention @user1
// - user1: hello
// - user2: mention @u1
// - user1: another repoy
// - user2: another mention @u1
// user1: a root post
// user2: Another root mention @u1
user1Mention := " @" + th.BasicUser.Username
rootPost1, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "first root mention" + user1Mention}, th.BasicChannel, false, false)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "hello"}, th.BasicChannel, false, false)
require.Nil(t, appErr)
replyPost1, appErr := th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "mention" + user1Mention}, th.BasicChannel, false, false)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another reply"}, th.BasicChannel, false, false)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another mention" + user1Mention}, th.BasicChannel, false, false)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "a root post"}, th.BasicChannel, false, false)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another root mention" + user1Mention}, th.BasicChannel, false, false)
require.Nil(t, appErr)
t.Run("Mark reply post as unread", func(t *testing.T) {
resp := th.Client.SetPostUnread(th.BasicUser.Id, replyPost1.Id, false)
CheckNoError(t, resp)
channelUnread, appErr := th.App.GetChannelUnread(th.BasicChannel.Id, th.BasicUser.Id)
require.Nil(t, appErr)
require.Equal(t, int64(3), channelUnread.MentionCount)
// MentionCountRoot should be zero so that supported clients don't show a mention badge for the channel
require.Equal(t, int64(0), channelUnread.MentionCountRoot)
require.Equal(t, int64(5), channelUnread.MsgCount)
// MentionCountRoot should be zero so that supported clients don't show the channel as unread
require.Equal(t, channelUnread.MsgCountRoot, int64(0))
thread, err := th.App.GetThreadForUser(th.BasicUser.Id, th.BasicTeam.Id, rootPost1.Id, false)
require.Nil(t, err)
require.Equal(t, int64(2), thread.UnreadMentions)
require.Equal(t, int64(3), thread.UnreadReplies)
})
t.Run("Mark root post as unread", func(t *testing.T) {
resp := th.Client.SetPostUnread(th.BasicUser.Id, rootPost1.Id, false)
CheckNoError(t, resp)
channelUnread, appErr := th.App.GetChannelUnread(th.BasicChannel.Id, th.BasicUser.Id)
require.Nil(t, appErr)
require.Equal(t, int64(4), channelUnread.MentionCount)
require.Equal(t, int64(2), channelUnread.MentionCountRoot)
require.Equal(t, int64(7), channelUnread.MsgCount)
require.Equal(t, int64(3), channelUnread.MsgCountRoot)
})
}

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

@@ -241,7 +241,7 @@ type AppIface interface {
// MakeAuditRecord creates a audit record pre-populated with defaults. // MakeAuditRecord creates a audit record pre-populated with defaults.
MakeAuditRecord(event string, initialStatus string) *audit.Record MakeAuditRecord(event string, initialStatus string) *audit.Record
// MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one. // MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one.
MarkChannelAsUnreadFromPost(postID string, userID string) (*model.ChannelUnreadAt, *model.AppError) MarkChannelAsUnreadFromPost(postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError)
// MentionsToPublicChannels returns all the mentions to public channels, // MentionsToPublicChannels returns all the mentions to public channels,
// linking them to their channels // linking them to their channels
MentionsToPublicChannels(message, teamID string) model.ChannelMentionMap MentionsToPublicChannels(message, teamID string) model.ChannelMentionMap
@@ -841,7 +841,7 @@ type AppIface interface {
Log() *mlog.Logger Log() *mlog.Logger
LoginByOAuth(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) LoginByOAuth(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError)
MakePermissionError(s *model.Session, permissions []*model.Permission) *model.AppError MakePermissionError(s *model.Session, permissions []*model.Permission) *model.AppError
MarkChannelsAsViewed(channelIDs []string, userID string, currentSessionId string) (map[string]int64, *model.AppError) MarkChannelsAsViewed(channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError)
MaxPostSize() int MaxPostSize() int
MessageExport() einterfaces.MessageExportInterface MessageExport() einterfaces.MessageExportInterface
Metrics() einterfaces.MetricsInterface Metrics() einterfaces.MetricsInterface
@@ -1086,6 +1086,6 @@ type AppIface interface {
UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError)
VerifyEmailFromToken(userSuppliedTokenString string) *model.AppError VerifyEmailFromToken(userSuppliedTokenString string) *model.AppError
VerifyUserEmail(userID, email string) *model.AppError VerifyUserEmail(userID, email string) *model.AppError
ViewChannel(view *model.ChannelView, userID string, currentSessionId string) (map[string]int64, *model.AppError) ViewChannel(view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError)
WriteFile(fr io.Reader, path string) (int64, *model.AppError) WriteFile(fr io.Reader, path string) (int64, *model.AppError)
} }

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

@@ -2378,7 +2378,10 @@ func (a *App) UpdateChannelLastViewedAt(channelIDs []string, userID string) *mod
} }
// MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one. // MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one.
func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string) (*model.ChannelUnreadAt, *model.AppError) { func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) {
if !collapsedThreadsSupported {
return a.markChannelAsUnreadFromPostCRTUnsupported(postID, userID)
}
post, err := a.GetSinglePost(postID) post, err := a.GetSinglePost(postID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -2394,7 +2397,9 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string) (*model.
return nil, err return nil, err
} }
if *a.Config().ServiceSettings.ThreadAutoFollow { // if auto-follow is on
// if threadmembership does not exists we create one and update
if *a.Config().ServiceSettings.ThreadAutoFollow && collapsedThreadsSupported {
threadId := post.RootId threadId := post.RootId
if post.RootId == "" { if post.RootId == "" {
threadId = post.Id threadId = post.Id
@@ -2434,25 +2439,127 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string) (*model.
} }
} }
} }
} }
channelUnread, nErr := a.Srv().Store.Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, *a.Config().ServiceSettings.ThreadAutoFollow) channelUnread, nErr := a.Srv().Store.Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, *a.Config().ServiceSettings.ThreadAutoFollow, true)
if nErr != nil { if nErr != nil {
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError) return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
} }
a.sendWebSocketPostUnreadEvent(channelUnread, postID, false)
a.UpdateMobileAppBadge(userID)
return channelUnread, nil
}
func (a *App) markChannelAsUnreadFromPostCRTUnsupported(postID string, userID string) (*model.ChannelUnreadAt, *model.AppError) {
post, err := a.GetSinglePost(postID)
if err != nil {
return nil, err
}
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
threadId := post.RootId
if post.RootId == "" {
threadId = post.Id
}
unreadMentions, unreadMentionsRoot, err := a.countMentionsFromPost(user, post)
if err != nil {
return nil, err
}
// if root post,
// In CRT Supported Client: badge on channel only sums mentions in root posts including and below the post that was marked.
// In CRT Unsupported Client: badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread.
if post.RootId == "" {
channelUnread, nErr := a.Srv().Store.Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, false, true)
if nErr != nil {
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
a.sendWebSocketPostUnreadEvent(channelUnread, postID, true)
a.UpdateMobileAppBadge(userID)
return channelUnread, nil
}
// if reply post, autofollow thread and
// In CRT Supported Client: Mark the specific thread as unread but not the channel where the thread exists.
// If there are replies with mentions below the marked reply in the thread, then sum the mentions for the threads mention badge.
// In CRT Unsupported Client: Channel is marked as unread and new messages line inserted above the marked post.
// Badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread.
rootPost, err := a.GetSinglePost(post.RootId)
if err != nil {
return nil, err
}
channel, nErr := a.Srv().Store.Channel().Get(post.ChannelId, true)
if nErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
threadMembership, nErr := a.Srv().Store.Thread().GetMembershipForUser(user.Id, threadId)
var errNotFound *store.ErrNotFound
if nErr != nil && !errors.As(nErr, &errNotFound) {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
// Follow thread if we're not already following it
if threadMembership == nil {
threadMembership, nErr = a.Srv().Store.Thread().MaintainMembership(user.Id, threadId, true, false, true, false, false)
if nErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
// If threadmembership already exists but user had previously unfollowed the thread, then follow the thread again.
threadMembership.Following = true
threadMembership.LastViewed = post.UpdateAt - 1
threadMembership.UnreadMentions, err = a.countThreadMentions(user, rootPost, channel.TeamId, post.UpdateAt-1)
if err != nil {
return nil, err
}
_, nErr = a.Srv().Store.Thread().UpdateMembership(threadMembership)
if nErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
thread, nErr := a.Srv().Store.Thread().GetThreadForUser(userID, channel.TeamId, threadId, true)
if nErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
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)
}
channelUnread, nErr := a.Srv().Store.Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, 0, false, false)
if nErr != nil {
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
return channelUnread, nil
}
func (a *App) sendWebSocketPostUnreadEvent(channelUnread *model.ChannelUnreadAt, postID string, withMsgCountRoot bool) {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_UNREAD, channelUnread.TeamId, channelUnread.ChannelId, channelUnread.UserId, nil) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_UNREAD, channelUnread.TeamId, channelUnread.ChannelId, channelUnread.UserId, nil)
message.Add("msg_count", channelUnread.MsgCount) message.Add("msg_count", channelUnread.MsgCount)
if withMsgCountRoot {
message.Add("msg_count_root", channelUnread.MsgCountRoot)
}
message.Add("mention_count", channelUnread.MentionCount) message.Add("mention_count", channelUnread.MentionCount)
message.Add("mention_count_root", channelUnread.MentionCountRoot) message.Add("mention_count_root", channelUnread.MentionCountRoot)
message.Add("last_viewed_at", channelUnread.LastViewedAt) message.Add("last_viewed_at", channelUnread.LastViewedAt)
message.Add("post_id", postID) message.Add("post_id", postID)
a.Publish(message) a.Publish(message)
a.UpdateMobileAppBadge(userID)
return channelUnread, nil
} }
func (a *App) AutocompleteChannels(teamID string, term string) (*model.ChannelList, *model.AppError) { func (a *App) AutocompleteChannels(teamID string, term string) (*model.ChannelList, *model.AppError) {
@@ -2571,7 +2678,7 @@ func (a *App) SearchChannelsUserNotIn(teamID string, userID string, term string)
return channelList, nil return channelList, nil
} }
func (a *App) MarkChannelsAsViewed(channelIDs []string, userID string, currentSessionId string) (map[string]int64, *model.AppError) { func (a *App) MarkChannelsAsViewed(channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
// I start looking for channels with notifications before I mark it as read, to clear the push notifications if needed // I start looking for channels with notifications before I mark it as read, to clear the push notifications if needed
channelsToClearPushNotifications := []string{} channelsToClearPushNotifications := []string{}
if *a.Config().EmailSettings.SendPushNotifications { if *a.Config().EmailSettings.SendPushNotifications {
@@ -2633,10 +2740,32 @@ func (a *App) MarkChannelsAsViewed(channelIDs []string, userID string, currentSe
for _, channelID := range channelsToClearPushNotifications { for _, channelID := range channelsToClearPushNotifications {
a.clearPushNotification(currentSessionId, userID, channelID) a.clearPushNotification(currentSessionId, userID, channelID)
} }
if !collapsedThreadsSupported {
// for compatibility with old clients, when channel is viewed - mark all threads in that channel as read
threadsEnabled := *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(userID, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_COLLAPSED_THREADS_ENABLED); err == nil {
threadsEnabled = preference.Value == "on"
}
if threadsEnabled {
if err := a.Srv().Store.Thread().MarkAllAsReadInChannels(userID, channelIDs); err != nil {
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, err.Error(), http.StatusInternalServerError)
}
timestamp := model.GetMillis()
for _, channelID := range channelIDs {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_READ_CHANGED, "", channelID, userID, nil)
message.Add("timestamp", timestamp)
a.Publish(message)
}
}
}
return times, nil return times, nil
} }
func (a *App) ViewChannel(view *model.ChannelView, userID string, currentSessionId string) (map[string]int64, *model.AppError) { func (a *App) ViewChannel(view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
if err := a.SetActiveChannel(userID, view.ChannelId); err != nil { if err := a.SetActiveChannel(userID, view.ChannelId); err != nil {
return nil, err return nil, err
} }
@@ -2655,7 +2784,7 @@ func (a *App) ViewChannel(view *model.ChannelView, userID string, currentSession
return map[string]int64{}, nil return map[string]int64{}, nil
} }
return a.MarkChannelsAsViewed(channelIDs, userID, currentSessionId) return a.MarkChannelsAsViewed(channelIDs, userID, currentSessionId, collapsedThreadsSupported)
} }
func (a *App) PermanentDeleteChannel(channel *model.Channel) *model.AppError { func (a *App) PermanentDeleteChannel(channel *model.Channel) *model.AppError {

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

@@ -13,6 +13,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/model"
@@ -1270,7 +1271,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) {
require.Equal(t, int64(0), unread.MsgCount) require.Equal(t, int64(0), unread.MsgCount)
t.Run("Unread but last one", func(t *testing.T) { t.Run("Unread but last one", func(t *testing.T) {
response, err := th.App.MarkChannelAsUnreadFromPost(p2.Id, u1.Id) response, err := th.App.MarkChannelAsUnreadFromPost(p2.Id, u1.Id, true)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, response) require.NotNil(t, response)
assert.Equal(t, int64(2), response.MsgCount) assert.Equal(t, int64(2), response.MsgCount)
@@ -1281,7 +1282,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) {
}) })
t.Run("Unread last one", func(t *testing.T) { t.Run("Unread last one", func(t *testing.T) {
response, err := th.App.MarkChannelAsUnreadFromPost(p3.Id, u1.Id) response, err := th.App.MarkChannelAsUnreadFromPost(p3.Id, u1.Id, true)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, response) require.NotNil(t, response)
assert.Equal(t, int64(3), response.MsgCount) assert.Equal(t, int64(3), response.MsgCount)
@@ -1292,7 +1293,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) {
}) })
t.Run("Unread first one", func(t *testing.T) { t.Run("Unread first one", func(t *testing.T) {
response, err := th.App.MarkChannelAsUnreadFromPost(p1.Id, u1.Id) response, err := th.App.MarkChannelAsUnreadFromPost(p1.Id, u1.Id, true)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, response) require.NotNil(t, response)
assert.Equal(t, int64(1), response.MsgCount) assert.Equal(t, int64(1), response.MsgCount)
@@ -1309,7 +1310,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) {
}) })
t.Run("Unread on a private channel", func(t *testing.T) { t.Run("Unread on a private channel", func(t *testing.T) {
response, err := th.App.MarkChannelAsUnreadFromPost(pp1.Id, u1.Id) response, err := th.App.MarkChannelAsUnreadFromPost(pp1.Id, u1.Id, true)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, response) require.NotNil(t, response)
assert.Equal(t, int64(0), response.MsgCount) assert.Equal(t, int64(0), response.MsgCount)
@@ -1318,7 +1319,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) {
assert.Equal(t, int64(2), unread.MsgCount) assert.Equal(t, int64(2), unread.MsgCount)
assert.Equal(t, pp1.CreateAt-1, response.LastViewedAt) assert.Equal(t, pp1.CreateAt-1, response.LastViewedAt)
response, err = th.App.MarkChannelAsUnreadFromPost(pp2.Id, u1.Id) response, err = th.App.MarkChannelAsUnreadFromPost(pp2.Id, u1.Id, true)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(1), response.MsgCount) assert.Equal(t, int64(1), response.MsgCount)
unread, err = th.App.GetChannelUnread(pc1.Id, u1.Id) unread, err = th.App.GetChannelUnread(pc1.Id, u1.Id)
@@ -1347,7 +1348,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) {
Message: "@" + u1.Username, Message: "@" + u1.Username,
}, c2, false, true) }, c2, false, true)
response, err := th.App.MarkChannelAsUnreadFromPost(p4.Id, u1.Id) response, err := th.App.MarkChannelAsUnreadFromPost(p4.Id, u1.Id, true)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(1), response.MsgCount) assert.Equal(t, int64(1), response.MsgCount)
assert.Equal(t, int64(2), response.MentionCount) assert.Equal(t, int64(2), response.MentionCount)
@@ -1370,7 +1371,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) {
_, err := th.App.CreatePost(th.Context, &model.Post{ChannelId: dc.Id, UserId: th.BasicUser.Id, Message: "testReply", RootId: dm1.Id}, dc, false, false) _, err := th.App.CreatePost(th.Context, &model.Post{ChannelId: dc.Id, UserId: th.BasicUser.Id, Message: "testReply", RootId: dm1.Id}, dc, false, false)
assert.Nil(t, err) assert.Nil(t, err)
response, err := th.App.MarkChannelAsUnreadFromPost(dm1.Id, u2.Id) response, err := th.App.MarkChannelAsUnreadFromPost(dm1.Id, u2.Id, true)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(0), response.MsgCount) assert.Equal(t, int64(0), response.MsgCount)
assert.Equal(t, int64(4), response.MentionCount) assert.Equal(t, int64(4), response.MentionCount)
@@ -1384,7 +1385,7 @@ func TestMarkChannelAsUnreadFromPost(t *testing.T) {
}) })
t.Run("Can't unread an imaginary post", func(t *testing.T) { t.Run("Can't unread an imaginary post", func(t *testing.T) {
response, err := th.App.MarkChannelAsUnreadFromPost("invalid4ofngungryquinj976y", u1.Id) response, err := th.App.MarkChannelAsUnreadFromPost("invalid4ofngungryquinj976y", u1.Id, true)
assert.NotNil(t, err) assert.NotNil(t, err)
assert.Nil(t, response) assert.Nil(t, response)
}) })
@@ -1966,10 +1967,13 @@ func TestMarkChannelsAsViewedPanic(t *testing.T) {
"userID": 1, "userID": 1,
} }
mockChannelStore.On("UpdateLastViewedAt", []string{"channelID"}, "userID", false).Return(times, nil) mockChannelStore.On("UpdateLastViewedAt", []string{"channelID"}, "userID", false).Return(times, nil)
mockPreferenceStore := mocks.PreferenceStore{}
mockPreferenceStore.On("Get", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&model.Preference{Value: "test"}, nil)
mockStore.On("User").Return(&mockUserStore) mockStore.On("User").Return(&mockUserStore)
mockStore.On("Channel").Return(&mockChannelStore) mockStore.On("Channel").Return(&mockChannelStore)
mockStore.On("Preference").Return(&mockPreferenceStore)
_, err := th.App.MarkChannelsAsViewed([]string{"channelID"}, "userID", th.Context.Session().Id) _, err := th.App.MarkChannelsAsViewed([]string{"channelID"}, "userID", th.Context.Session().Id, false)
require.Nil(t, err) require.Nil(t, err)
} }

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

@@ -11221,7 +11221,7 @@ func (a *OpenTracingAppLayer) MakePermissionError(s *model.Session, permissions
return resultVar0 return resultVar0
} }
func (a *OpenTracingAppLayer) MarkChannelAsUnreadFromPost(postID string, userID string) (*model.ChannelUnreadAt, *model.AppError) { func (a *OpenTracingAppLayer) MarkChannelAsUnreadFromPost(postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MarkChannelAsUnreadFromPost") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MarkChannelAsUnreadFromPost")
@@ -11233,7 +11233,7 @@ func (a *OpenTracingAppLayer) MarkChannelAsUnreadFromPost(postID string, userID
}() }()
defer span.Finish() defer span.Finish()
resultVar0, resultVar1 := a.app.MarkChannelAsUnreadFromPost(postID, userID) resultVar0, resultVar1 := a.app.MarkChannelAsUnreadFromPost(postID, userID, collapsedThreadsSupported)
if resultVar1 != nil { if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1)) span.LogFields(spanlog.Error(resultVar1))
@@ -11243,7 +11243,7 @@ func (a *OpenTracingAppLayer) MarkChannelAsUnreadFromPost(postID string, userID
return resultVar0, resultVar1 return resultVar0, resultVar1
} }
func (a *OpenTracingAppLayer) MarkChannelsAsViewed(channelIDs []string, userID string, currentSessionId string) (map[string]int64, *model.AppError) { func (a *OpenTracingAppLayer) MarkChannelsAsViewed(channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MarkChannelsAsViewed") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MarkChannelsAsViewed")
@@ -11255,7 +11255,7 @@ func (a *OpenTracingAppLayer) MarkChannelsAsViewed(channelIDs []string, userID s
}() }()
defer span.Finish() defer span.Finish()
resultVar0, resultVar1 := a.app.MarkChannelsAsViewed(channelIDs, userID, currentSessionId) resultVar0, resultVar1 := a.app.MarkChannelsAsViewed(channelIDs, userID, currentSessionId, collapsedThreadsSupported)
if resultVar1 != nil { if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1)) span.LogFields(spanlog.Error(resultVar1))
@@ -17079,7 +17079,7 @@ func (a *OpenTracingAppLayer) VerifyUserEmail(userID string, email string) *mode
return resultVar0 return resultVar0
} }
func (a *OpenTracingAppLayer) ViewChannel(view *model.ChannelView, userID string, currentSessionId string) (map[string]int64, *model.AppError) { func (a *OpenTracingAppLayer) ViewChannel(view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ViewChannel") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ViewChannel")
@@ -17091,7 +17091,7 @@ func (a *OpenTracingAppLayer) ViewChannel(view *model.ChannelView, userID string
}() }()
defer span.Finish() defer span.Finish()
resultVar0, resultVar1 := a.app.ViewChannel(view, userID, currentSessionId) resultVar0, resultVar1 := a.app.ViewChannel(view, userID, currentSessionId, collapsedThreadsSupported)
if resultVar1 != nil { if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1)) span.LogFields(spanlog.Error(resultVar1))

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

@@ -88,7 +88,7 @@ func (a *App) CreatePostAsUser(c *request.Context, post *model.Post, currentSess
_, fromWebhook := post.GetProps()["from_webhook"] _, fromWebhook := post.GetProps()["from_webhook"]
_, fromBot := post.GetProps()["from_bot"] _, fromBot := post.GetProps()["from_bot"]
if !fromWebhook && !fromBot { if !fromWebhook && !fromBot {
if _, err := a.MarkChannelsAsViewed([]string{post.ChannelId}, post.UserId, currentSessionId); err != nil { if _, err := a.MarkChannelsAsViewed([]string{post.ChannelId}, post.UserId, currentSessionId, true); err != nil {
mlog.Warn( mlog.Warn(
"Encountered error updating last viewed", "Encountered error updating last viewed",
mlog.String("channel_id", post.ChannelId), mlog.String("channel_id", post.ChannelId),

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

@@ -2032,7 +2032,7 @@ func TestViewChannelShouldNotUpdateThreads(t *testing.T) {
th.App.ViewChannel(&model.ChannelView{ th.App.ViewChannel(&model.ChannelView{
ChannelId: channel.Id, ChannelId: channel.Id,
PrevChannelId: "", PrevChannelId: "",
}, user2.Id, "") }, user2.Id, "", true)
m1, e1 := th.App.GetThreadMembershipsForUser(user2.Id, th.BasicTeam.Id) m1, e1 := th.App.GetThreadMembershipsForUser(user2.Id, th.BasicTeam.Id)
require.NoError(t, e1) require.NoError(t, e1)
@@ -2072,7 +2072,7 @@ func TestCollapsedThreadFetch(t *testing.T) {
thread, nErr := th.App.Srv().Store.Thread().Get(postRoot.Id) thread, nErr := th.App.Srv().Store.Thread().Get(postRoot.Id)
require.NoError(t, nErr) require.NoError(t, nErr)
require.Len(t, thread.Participants, 1) require.Len(t, thread.Participants, 1)
th.App.MarkChannelAsUnreadFromPost(postRoot.Id, user1.Id) th.App.MarkChannelAsUnreadFromPost(postRoot.Id, user1.Id, true)
l, err := th.App.GetPostsForChannelAroundLastUnread(channel.Id, user1.Id, 10, 10, true, true, false) l, err := th.App.GetPostsForChannelAroundLastUnread(channel.Id, user1.Id, 10, 10, true, true, false)
require.Nil(t, err) require.Nil(t, err)
require.Len(t, l.Order, 1) require.Len(t, l.Order, 1)

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

@@ -9,8 +9,9 @@ import (
) )
type ChannelView struct { type ChannelView struct {
ChannelId string `json:"channel_id"` ChannelId string `json:"channel_id"`
PrevChannelId string `json:"prev_channel_id"` PrevChannelId string `json:"prev_channel_id"`
CollapsedThreadsSupported bool `json:"collapsed_threads_supported"`
} }
func (o *ChannelView) ToJson() string { func (o *ChannelView) ToJson() string {

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

@@ -2915,8 +2915,9 @@ func (c *Client4) PatchPost(postId string, patch *PostPatch) (*Post, *Response)
} }
// SetPostUnread marks channel where post belongs as unread on the time of the provided post. // SetPostUnread marks channel where post belongs as unread on the time of the provided post.
func (c *Client4) SetPostUnread(userId string, postId string) *Response { func (c *Client4) SetPostUnread(userId string, postId string, collapsedThreadsSupported bool) *Response {
r, err := c.DoApiPost(c.GetUserRoute(userId)+c.GetPostRoute(postId)+"/set_unread", "") b, _ := json.Marshal(map[string]bool{"collapsed_threads_supported": collapsedThreadsSupported})
r, err := c.DoApiPost(c.GetUserRoute(userId)+c.GetPostRoute(postId)+"/set_unread", string(b))
if err != nil { if err != nil {
return BuildErrorResponse(r, err) return BuildErrorResponse(r, err)
} }

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

@@ -2176,7 +2176,7 @@ func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, u
return result, err return result, err
} }
func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, updateThreads bool) (*model.ChannelUnreadAt, error) { func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, updateThreads bool, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
origCtx := s.Root.Store.Context() origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateLastViewedAtPost") span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateLastViewedAtPost")
s.Root.Store.SetContext(newCtx) s.Root.Store.SetContext(newCtx)
@@ -2185,7 +2185,7 @@ func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.
}() }()
defer span.Finish() defer span.Finish()
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads) result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads, setUnreadCountRoot)
if err != nil { if err != nil {
span.LogFields(spanlog.Error(err)) span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true) ext.Error.Set(span, true)
@@ -8938,6 +8938,24 @@ func (s *OpenTracingLayerThreadStore) MarkAllAsRead(userID string, teamID string
return err return err
} }
func (s *OpenTracingLayerThreadStore) MarkAllAsReadInChannels(userID string, channelIDs []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MarkAllAsReadInChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.MarkAllAsReadInChannels(userID, channelIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error { func (s *OpenTracingLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
origCtx := s.Root.Store.Context() origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MarkAsRead") span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MarkAsRead")

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

@@ -2308,11 +2308,11 @@ func (s *RetryLayerChannelStore) UpdateLastViewedAt(channelIds []string, userID
} }
func (s *RetryLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, updateThreads bool) (*model.ChannelUnreadAt, error) { func (s *RetryLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, updateThreads bool, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
tries := 0 tries := 0
for { for {
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads) result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads, setUnreadCountRoot)
if err == nil { if err == nil {
return result, nil return result, nil
} }
@@ -9728,6 +9728,26 @@ func (s *RetryLayerThreadStore) MarkAllAsRead(userID string, teamID string) erro
} }
func (s *RetryLayerThreadStore) MarkAllAsReadInChannels(userID string, channelIDs []string) error {
tries := 0
for {
err := s.ThreadStore.MarkAllAsReadInChannels(userID, channelIDs)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
}
}
func (s *RetryLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error { func (s *RetryLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
tries := 0 tries := 0

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

@@ -2216,7 +2216,7 @@ func (s SqlChannelStore) CountPostsAfter(channelId string, timestamp int64, user
// UpdateLastViewedAtPost updates a ChannelMember as if the user last read the channel at the time of the given post. // UpdateLastViewedAtPost updates a ChannelMember as if the user last read the channel at the time of the given post.
// If the provided mentionCount is -1, the given post and all posts after it are considered to be mentions. Returns // If the provided mentionCount is -1, the given post and all posts after it are considered to be mentions. Returns
// an updated model.ChannelUnreadAt that can be returned to the client. // an updated model.ChannelUnreadAt that can be returned to the client.
func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot int, updateThreads bool) (*model.ChannelUnreadAt, error) { func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot int, updateThreads bool, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
var threadsToUpdate []string var threadsToUpdate []string
unreadDate := unreadPost.CreateAt - 1 unreadDate := unreadPost.CreateAt - 1
if updateThreads { if updateThreads {
@@ -2232,6 +2232,10 @@ func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID s
return nil, err return nil, err
} }
if !setUnreadCountRoot {
unreadRoot = 0
}
params := map[string]interface{}{ params := map[string]interface{}{
"mentions": mentionCount, "mentions": mentionCount,
"mentionsRoot": mentionCountRoot, "mentionsRoot": mentionCountRoot,

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

@@ -386,7 +386,37 @@ func (s *SqlThreadStore) GetThreadForUser(userId, teamId, threadId string, exten
return result, nil return result, nil
} }
func (s *SqlThreadStore) MarkAllAsReadInChannels(userID string, channelIDs []string) error {
var threadIDs []string
query, args, _ := s.getQueryBuilder().
Select("ThreadMemberships.PostId").
Join("Threads ON Threads.PostId = ThreadMemberships.PostId").
Join("Channels ON Threads.ChannelId = Channels.Id").
From("ThreadMemberships").
Where(sq.Eq{"Threads.ChannelId": channelIDs}).
Where(sq.Eq{"ThreadMemberships.UserId": userID}).
ToSql()
_, err := s.GetReplica().Select(&threadIDs, query, args...)
if err != nil {
return errors.Wrapf(err, "failed to get thread membership with userid=%s", userID)
}
timestamp := model.GetMillis()
query, args, _ = s.getQueryBuilder().
Update("ThreadMemberships").
Where(sq.Eq{"PostId": threadIDs}).
Where(sq.Eq{"UserId": userID}).
Set("LastViewed", timestamp).
Set("UnreadMentions", 0).
ToSql()
if _, err := s.GetMaster().Exec(query, args...); err != nil {
return errors.Wrapf(err, "failed to update thread read state for user id=%s", userID)
}
return nil
}
func (s *SqlThreadStore) MarkAllAsRead(userId, teamId string) error { func (s *SqlThreadStore) MarkAllAsRead(userId, teamId string) error {
memberships, err := s.GetMembershipsForUser(userId, teamId) memberships, err := s.GetMembershipsForUser(userId, teamId)
if err != nil { if err != nil {

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

@@ -216,7 +216,7 @@ type ChannelStore interface {
PermanentDeleteMembersByUser(userID string) error PermanentDeleteMembersByUser(userID string) error
PermanentDeleteMembersByChannel(channelID string) error PermanentDeleteMembersByChannel(channelID string) error
UpdateLastViewedAt(channelIds []string, userID string, updateThreads bool) (map[string]int64, error) UpdateLastViewedAt(channelIds []string, userID string, updateThreads bool) (map[string]int64, error)
UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot int, updateThreads bool) (*model.ChannelUnreadAt, error) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot int, updateThreads bool, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error)
CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error) CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error)
IncrementMentionCount(channelID string, userID string, updateThreads, isRoot bool) error IncrementMentionCount(channelID string, userID string, updateThreads, isRoot bool) error
AnalyticsTypeCount(teamID string, channelType string) (int64, error) AnalyticsTypeCount(teamID string, channelType string) (int64, error)
@@ -288,6 +288,7 @@ type ThreadStore interface {
GetPosts(threadID string, since int64) ([]*model.Post, error) GetPosts(threadID string, since int64) ([]*model.Post, error)
MarkAllAsRead(userID, teamID string) error MarkAllAsRead(userID, teamID string) error
MarkAllAsReadInChannels(userID string, channelIDs []string) error
MarkAsRead(userID, threadID string, timestamp int64) error MarkAsRead(userID, threadID string, timestamp int64) error
SaveMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) SaveMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error)

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

@@ -1861,13 +1861,13 @@ func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userID string, u
return r0, r1 return r0, r1
} }
// UpdateLastViewedAtPost provides a mock function with given fields: unreadPost, userID, mentionCount, mentionCountRoot, updateThreads // UpdateLastViewedAtPost provides a mock function with given fields: unreadPost, userID, mentionCount, mentionCountRoot, updateThreads, setUnreadCountRoot
func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, updateThreads bool) (*model.ChannelUnreadAt, error) { func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, updateThreads bool, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
ret := _m.Called(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads) ret := _m.Called(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads, setUnreadCountRoot)
var r0 *model.ChannelUnreadAt var r0 *model.ChannelUnreadAt
if rf, ok := ret.Get(0).(func(*model.Post, string, int, int, bool) *model.ChannelUnreadAt); ok { if rf, ok := ret.Get(0).(func(*model.Post, string, int, int, bool, bool) *model.ChannelUnreadAt); ok {
r0 = rf(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads) r0 = rf(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads, setUnreadCountRoot)
} else { } else {
if ret.Get(0) != nil { if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelUnreadAt) r0 = ret.Get(0).(*model.ChannelUnreadAt)
@@ -1875,8 +1875,8 @@ func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID st
} }
var r1 error var r1 error
if rf, ok := ret.Get(1).(func(*model.Post, string, int, int, bool) error); ok { if rf, ok := ret.Get(1).(func(*model.Post, string, int, int, bool, bool) error); ok {
r1 = rf(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads) r1 = rf(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads, setUnreadCountRoot)
} else { } else {
r1 = ret.Error(1) r1 = ret.Error(1)
} }

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

@@ -263,6 +263,20 @@ func (_m *ThreadStore) MarkAllAsRead(userID string, teamID string) error {
return r0 return r0
} }
// MarkAllAsReadInChannels provides a mock function with given fields: userID, channelIDs
func (_m *ThreadStore) MarkAllAsReadInChannels(userID string, channelIDs []string) error {
ret := _m.Called(userID, channelIDs)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(userID, channelIDs)
} else {
r0 = ret.Error(0)
}
return r0
}
// MarkAsRead provides a mock function with given fields: userID, threadID, timestamp // MarkAsRead provides a mock function with given fields: userID, threadID, timestamp
func (_m *ThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error { func (_m *ThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
ret := _m.Called(userID, threadID, timestamp) ret := _m.Called(userID, threadID, timestamp)

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

@@ -243,7 +243,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) {
_, err := ss.Thread().UpdateMembership(m) _, err := ss.Thread().UpdateMembership(m)
require.NoError(t, err) require.NoError(t, err)
_, err = ss.Channel().UpdateLastViewedAtPost(newPosts[0], newPosts[0].UserId, 0, 0, true) _, err = ss.Channel().UpdateLastViewedAtPost(newPosts[0], newPosts[0].UserId, 0, 0, true, true)
require.NoError(t, err) require.NoError(t, err)
assert.Eventually(t, func() bool { assert.Eventually(t, func() bool {
@@ -325,7 +325,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) {
_, err := ss.Thread().UpdateMembership(m) _, err := ss.Thread().UpdateMembership(m)
require.NoError(t, err) require.NoError(t, err)
_, err = ss.Channel().UpdateLastViewedAtPost(newPosts[0], newPosts[0].UserId, 0, 0, true) _, err = ss.Channel().UpdateLastViewedAtPost(newPosts[0], newPosts[0].UserId, 0, 0, true, true)
require.NoError(t, err) require.NoError(t, err)
assert.Eventually(t, func() bool { assert.Eventually(t, func() bool {

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

@@ -2014,10 +2014,10 @@ func (s *TimerLayerChannelStore) UpdateLastViewedAt(channelIds []string, userID
return result, err return result, err
} }
func (s *TimerLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, updateThreads bool) (*model.ChannelUnreadAt, error) { func (s *TimerLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, updateThreads bool, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
start := timemodule.Now() start := timemodule.Now()
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads) result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, updateThreads, setUnreadCountRoot)
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
if s.Root.Metrics != nil { if s.Root.Metrics != nil {
@@ -8054,6 +8054,22 @@ func (s *TimerLayerThreadStore) MarkAllAsRead(userID string, teamID string) erro
return err return err
} }
func (s *TimerLayerThreadStore) MarkAllAsReadInChannels(userID string, channelIDs []string) error {
start := timemodule.Now()
err := s.ThreadStore.MarkAllAsReadInChannels(userID, channelIDs)
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MarkAllAsReadInChannels", success, elapsed)
}
return err
}
func (s *TimerLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error { func (s *TimerLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
start := timemodule.Now() start := timemodule.Now()