MM-29703 Mark threads as read when channels are marked (#15994)

Co-authored-by: Jesús Espino <jespinog@gmail.com>
Этот коммит содержится в:
Eli Yukelzon
2020-10-30 17:00:21 +02:00
коммит произвёл GitHub
родитель 1729239385
Коммит fe352ab57f
20 изменённых файлов: 451 добавлений и 92 удалений

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

@@ -2322,7 +2322,7 @@ func (a *App) SetActiveChannel(userId string, channelId string) *model.AppError
}
func (a *App) UpdateChannelLastViewedAt(channelIds []string, userId string) *model.AppError {
if _, err := a.Srv().Store.Channel().UpdateLastViewedAt(channelIds, userId); err != nil {
if _, err := a.Srv().Store.Channel().UpdateLastViewedAt(channelIds, userId, *a.Config().ServiceSettings.ThreadAutoFollow); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
@@ -2360,7 +2360,7 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string) (*model.
return nil, err
}
channelUnread, nErr := a.Srv().Store.Channel().UpdateLastViewedAtPost(post, userID, unreadMentions)
channelUnread, nErr := a.Srv().Store.Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, *a.Config().ServiceSettings.ThreadAutoFollow)
if nErr != nil {
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
@@ -2531,7 +2531,7 @@ func (a *App) MarkChannelsAsViewed(channelIds []string, userId string, currentSe
}
}
}
times, err := a.Srv().Store.Channel().UpdateLastViewedAt(channelIds, userId)
times, err := a.Srv().Store.Channel().UpdateLastViewedAt(channelIds, userId, *a.Config().ServiceSettings.ThreadAutoFollow)
if err != nil {
var invErr *store.ErrInvalidInput
switch {

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

@@ -1869,7 +1869,7 @@ func TestMarkChannelsAsViewedPanic(t *testing.T) {
times := map[string]int64{
"userID": 1,
}
mockChannelStore.On("UpdateLastViewedAt", []string{"channelID"}, "userID").Return(times, nil)
mockChannelStore.On("UpdateLastViewedAt", []string{"channelID"}, "userID", true).Return(times, nil)
mockStore.On("User").Return(&mockUserStore)
mockStore.On("Channel").Return(&mockChannelStore)

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

@@ -184,7 +184,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
umc := make(chan *model.AppError, 1)
go func(userId string) {
defer close(umc)
nErr := a.Srv().Store.Channel().IncrementMentionCount(post.ChannelId, userId)
nErr := a.Srv().Store.Channel().IncrementMentionCount(post.ChannelId, userId, *a.Config().ServiceSettings.ThreadAutoFollow)
if nErr != nil {
umc <- model.NewAppError("SendNotifications", "app.channel.increment_mention_count.app_error", nil, nErr.Error(), http.StatusInternalServerError)
return

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

@@ -71,13 +71,13 @@ func (_m *LdapInterface) DoLogin(id string, password string) (*model.User, *mode
return r0, r1
}
// FirstLoginSync provides a mock function with given fields: userID, userAuthService, userAuthData, email
func (_m *LdapInterface) FirstLoginSync(userID string, userAuthService string, userAuthData string, email string) *model.AppError {
ret := _m.Called(userID, userAuthService, userAuthData, email)
// FirstLoginSync provides a mock function with given fields: user, userAuthService, userAuthData, email
func (_m *LdapInterface) FirstLoginSync(user *model.User, userAuthService string, userAuthData string, email string) *model.AppError {
ret := _m.Called(user, userAuthService, userAuthData, email)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string, string, string, string) *model.AppError); ok {
r0 = rf(userID, userAuthService, userAuthData, email)
if rf, ok := ret.Get(0).(func(*model.User, string, string, string) *model.AppError); ok {
r0 = rf(user, userAuthService, userAuthData, email)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)

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

@@ -17,7 +17,7 @@ type OauthProvider struct {
}
// GetUserFromJson provides a mock function with given fields: data
func (_m *OauthProvider) GetUserFromJson(data io.Reader) *model.User {
func (_m *OauthProvider) GetUserFromJson(data io.Reader) (*model.User, error) {
ret := _m.Called(data)
var r0 *model.User
@@ -29,5 +29,12 @@ func (_m *OauthProvider) GetUserFromJson(data io.Reader) *model.User {
}
}
return r0
var r1 error
if rf, ok := ret.Get(1).(func(io.Reader) error); ok {
r1 = rf(data)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

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

@@ -9,6 +9,7 @@ import (
type Thread struct {
PostId string `json:"id"`
ChannelId string `json:"channel_id"`
ReplyCount int64 `json:"reply_count"`
LastReplyAt int64 `json:"last_reply_at"`
Participants StringArray `json:"participants"`

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

@@ -1538,7 +1538,7 @@ func (s *OpenTracingLayerChannelStore) GroupSyncedChannelCount() (int64, error)
return result, err
}
func (s *OpenTracingLayerChannelStore) IncrementMentionCount(channelId string, userId string) error {
func (s *OpenTracingLayerChannelStore) IncrementMentionCount(channelId string, userId string, updateThreads bool) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.IncrementMentionCount")
s.Root.Store.SetContext(newCtx)
@@ -1547,7 +1547,7 @@ func (s *OpenTracingLayerChannelStore) IncrementMentionCount(channelId string, u
}()
defer span.Finish()
err := s.ChannelStore.IncrementMentionCount(channelId, userId)
err := s.ChannelStore.IncrementMentionCount(channelId, userId, updateThreads)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
@@ -2074,7 +2074,7 @@ func (s *OpenTracingLayerChannelStore) Update(channel *model.Channel) (*model.Ch
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) {
func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateLastViewedAt")
s.Root.Store.SetContext(newCtx)
@@ -2083,7 +2083,7 @@ func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, u
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId)
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId, updateThreads)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
@@ -2092,7 +2092,7 @@ func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, u
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, error) {
func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateLastViewedAtPost")
s.Root.Store.SetContext(newCtx)
@@ -2101,7 +2101,7 @@ func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount)
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, updateThreads)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
@@ -7612,6 +7612,24 @@ func (s *OpenTracingLayerTermsOfServiceStore) Save(termsOfService *model.TermsOf
return result, err
}
func (s *OpenTracingLayerThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.CollectThreadsWithNewerReplies")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.CollectThreadsWithNewerReplies(userId, channelIds, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.CreateMembershipIfNeeded")
@@ -7810,6 +7828,24 @@ func (s *OpenTracingLayerThreadStore) UpdateMembership(membership *model.ThreadM
return result, err
}
func (s *OpenTracingLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.UpdateUnreadsByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTokenStore) Cleanup() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.Cleanup")

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

@@ -1670,11 +1670,11 @@ func (s *RetryLayerChannelStore) GroupSyncedChannelCount() (int64, error) {
}
func (s *RetryLayerChannelStore) IncrementMentionCount(channelId string, userId string) error {
func (s *RetryLayerChannelStore) IncrementMentionCount(channelId string, userId string, updateThreads bool) error {
tries := 0
for {
err := s.ChannelStore.IncrementMentionCount(channelId, userId)
err := s.ChannelStore.IncrementMentionCount(channelId, userId, updateThreads)
if err == nil {
return nil
}
@@ -2198,11 +2198,11 @@ func (s *RetryLayerChannelStore) Update(channel *model.Channel) (*model.Channel,
}
func (s *RetryLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) {
func (s *RetryLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId)
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId, updateThreads)
if err == nil {
return result, nil
}
@@ -2218,11 +2218,11 @@ func (s *RetryLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId
}
func (s *RetryLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, error) {
func (s *RetryLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount)
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, updateThreads)
if err == nil {
return result, nil
}
@@ -7642,6 +7642,26 @@ func (s *RetryLayerTermsOfServiceStore) Save(termsOfService *model.TermsOfServic
}
func (s *RetryLayerThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) {
tries := 0
for {
result, err := s.ThreadStore.CollectThreadsWithNewerReplies(userId, channelIds, timestamp)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
}
}
func (s *RetryLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string) error {
tries := 0
@@ -7862,6 +7882,26 @@ func (s *RetryLayerThreadStore) UpdateMembership(membership *model.ThreadMembers
}
func (s *RetryLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error {
tries := 0
for {
err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp)
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 *RetryLayerTokenStore) Cleanup() {
s.TokenStore.Cleanup()

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

@@ -2046,7 +2046,17 @@ func (s SqlChannelStore) PermanentDeleteMembersByUser(userId string) error {
return nil
}
func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) {
func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error) {
var threadsToUpdate []string
now := model.GetMillis()
if updateThreads {
var err error
threadsToUpdate, err = s.Thread().CollectThreadsWithNewerReplies(userId, channelIds, now)
if err != nil {
return nil, err
}
}
keys, props := MapStringsToQueryParams(channelIds, "Channel")
props["UserId"] = userId
@@ -2089,11 +2099,15 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string)
for _, t := range lastPostAtTimes {
times[t.Id] = t.LastPostAt
}
if updateThreads {
s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now)
}
return times, nil
}
msgCountQuery := ""
lastViewedQuery := ""
for index, t := range lastPostAtTimes {
times[t.Id] = t.LastPostAt
@@ -2121,6 +2135,9 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string)
return nil, errors.Wrapf(err, "failed to update ChannelMembers with userId=%s and channelId in %v", userId, channelIds)
}
if updateThreads {
s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now)
}
return times, nil
}
@@ -2168,8 +2185,16 @@ 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.
// 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.
func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, error) {
func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error) {
var threadsToUpdate []string
unreadDate := unreadPost.CreateAt - 1
if updateThreads {
var err error
threadsToUpdate, err = s.Thread().CollectThreadsWithNewerReplies(userID, []string{unreadPost.ChannelId}, unreadDate)
if err != nil {
return nil, err
}
}
unread, err := s.CountPostsAfter(unreadPost.ChannelId, unreadDate, "")
if err != nil {
@@ -2225,10 +2250,24 @@ func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID s
if err = s.GetMaster().SelectOne(result, chanUnreadQuery, params); err != nil {
return nil, errors.Wrapf(err, "failed to get ChannelMember with channelId=%s", unreadPost.ChannelId)
}
if updateThreads {
s.Thread().UpdateUnreadsByChannel(userID, threadsToUpdate, unreadDate)
}
return result, nil
}
func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string) error {
func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string, updateThreads bool) error {
now := model.GetMillis()
var threadsToUpdate []string
if updateThreads {
var err error
threadsToUpdate, err = s.Thread().CollectThreadsWithNewerReplies(userId, []string{channelId}, now)
if err != nil {
return err
}
}
_, err := s.GetMaster().Exec(
`UPDATE
ChannelMembers
@@ -2238,11 +2277,13 @@ func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string)
WHERE
UserId = :UserId
AND ChannelId = :ChannelId`,
map[string]interface{}{"ChannelId": channelId, "UserId": userId, "LastUpdateAt": model.GetMillis()})
map[string]interface{}{"ChannelId": channelId, "UserId": userId, "LastUpdateAt": now})
if err != nil {
return errors.Wrapf(err, "failed to Update ChannelMembers with channelId=%s and userId=%s", channelId, userId)
}
if updateThreads {
s.Thread().UpdateUnreadsByChannel(userId, threadsToUpdate, now)
}
return nil
}

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

@@ -1987,6 +1987,7 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *gorp.Transaction, pos
// no metadata entry, create one
if err := transaction.Insert(&model.Thread{
PostId: rootId,
ChannelId: posts[0].ChannelId,
ReplyCount: count,
LastReplyAt: now,
Participants: participants,

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

@@ -5,11 +5,12 @@ package sqlstore
import (
"database/sql"
"time"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/store"
"github.com/mattermost/mattermost-server/v5/utils"
"github.com/pkg/errors"
"time"
sq "github.com/Masterminds/squirrel"
)
@@ -29,6 +30,7 @@ func newSqlThreadStore(sqlStore SqlStore) store.ThreadStore {
for _, db := range sqlStore.GetAllConns() {
tableThreads := db.AddTableWithName(model.Thread{}, "Threads").SetKeys(false, "PostId")
tableThreads.ColMap("PostId").SetMaxSize(26)
tableThreads.ColMap("ChannelId").SetMaxSize(26)
tableThreads.ColMap("Participants").SetMaxSize(0)
tableThreadMemberships := db.AddTableWithName(model.ThreadMembership{}, "ThreadMemberships").SetKeys(false, "PostId", "UserId")
tableThreadMemberships.ColMap("PostId").SetMaxSize(26)
@@ -39,12 +41,13 @@ func newSqlThreadStore(sqlStore SqlStore) store.ThreadStore {
}
func threadSliceColumns() []string {
return []string{"PostId", "LastReplyAt", "ReplyCount", "Participants"}
return []string{"PostId", "ChannelId", "LastReplyAt", "ReplyCount", "Participants"}
}
func threadToSlice(thread *model.Thread) []interface{} {
return []interface{}{
thread.PostId,
thread.ChannelId,
thread.LastReplyAt,
thread.ReplyCount,
thread.Participants,
@@ -52,13 +55,10 @@ func threadToSlice(thread *model.Thread) []interface{} {
}
func (s *SqlThreadStore) createIndexesIfNotExists() {
s.CreateIndexIfNotExists("idx_threads_last_reply_at", "Threads", "LastReplyAt")
s.CreateIndexIfNotExists("idx_threads_post_id", "Threads", "PostId")
s.CreateIndexIfNotExists("idx_thread_memberships_last_update_at", "ThreadMemberships", "LastUpdated")
s.CreateIndexIfNotExists("idx_thread_memberships_last_view_at", "ThreadMemberships", "LastViewed")
s.CreateIndexIfNotExists("idx_thread_memberships_post_id", "ThreadMemberships", "PostId")
s.CreateIndexIfNotExists("idx_thread_memberships_user_id", "ThreadMemberships", "UserId")
s.CreateIndexIfNotExists("idx_threads_channel_id", "Threads", "ChannelId")
}
func (s *SqlThreadStore) SaveMultiple(threads []*model.Thread) ([]*model.Thread, int, error) {
@@ -188,3 +188,41 @@ func (s *SqlThreadStore) CreateMembershipIfNeeded(userId, postId string) error {
})
return err
}
func (s *SqlThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) {
var changedThreads []string
query, args, _ := s.getQueryBuilder().
Select("Threads.PostId").
From("Threads").
LeftJoin("ChannelMembers ON ChannelMembers.ChannelId=Threads.ChannelId").
Where(sq.And{
sq.Eq{"Threads.ChannelId": channelIds},
sq.Eq{"ChannelMembers.UserId": userId},
sq.Or{
sq.Expr("Threads.LastReplyAt >= ChannelMembers.LastViewedAt"),
sq.GtOrEq{"Threads.LastReplyAt": timestamp},
},
}).
ToSql()
if _, err := s.GetReplica().Select(&changedThreads, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to fetch threads")
}
return changedThreads, nil
}
func (s *SqlThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error {
if len(changedThreads) == 0 {
return nil
}
updateQuery, updateArgs, _ := s.getQueryBuilder().
Update("ThreadMemberships").
Where(sq.Eq{"UserId": userId, "PostId": changedThreads}).
Set("LastUpdated", timestamp).
Set("LastViewed", timestamp).
ToSql()
if _, err := s.GetMaster().Exec(updateQuery, updateArgs...); err != nil {
return errors.Wrap(err, "failed to update thread membership")
}
return nil
}

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

@@ -925,6 +925,16 @@ func upgradeDatabaseToVersion529(sqlStore SqlStore) {
sqlStore.AlterColumnTypeIfExists("SidebarChannels", "CategoryId", "VARCHAR(128)", "VARCHAR(128)")
sqlStore.AlterColumnDefaultIfExists("SidebarChannels", "CategoryId", model.NewString(""), nil)
sqlStore.CreateColumnIfNotExistsNoDefault("Threads", "ChannelId", "VARCHAR(26)", "VARCHAR(26)")
updateThreadChannelsQuery := "UPDATE Threads INNER JOIN Posts ON Posts.Id=Threads.PostId SET Threads.ChannelId=Posts.ChannelId WHERE Threads.ChannelId IS NULL"
if sqlStore.DriverName() == model.DATABASE_DRIVER_POSTGRES {
updateThreadChannelsQuery = "UPDATE Threads SET ChannelId=Posts.ChannelId FROM Posts WHERE Posts.Id=Threads.PostId AND Threads.ChannelId IS NULL"
}
if _, err := sqlStore.GetMaster().Exec(updateThreadChannelsQuery); err != nil {
mlog.Error("Error updating ChannelId in Threads table", mlog.Err(err))
}
saveSchemaVersion(sqlStore, VERSION_5_29_0)
}
}

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

@@ -191,10 +191,10 @@ type ChannelStore interface {
RemoveMembers(channelId string, userIds []string) error
PermanentDeleteMembersByUser(userId string) error
PermanentDeleteMembersByChannel(channelId string) error
UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error)
UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, error)
UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error)
UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error)
CountPostsAfter(channelId string, timestamp int64, userId string) (int, error)
IncrementMentionCount(channelId string, userId string) error
IncrementMentionCount(channelId string, userId string, updateThreads bool) error
AnalyticsTypeCount(teamId string, channelType string) (int64, error)
GetMembersForUser(teamId string, userId string) (*model.ChannelMembers, error)
GetMembersForUserWithPagination(teamId, userId string, page, perPage int) (*model.ChannelMembers, error)
@@ -259,6 +259,8 @@ type ThreadStore interface {
GetMembershipForUser(userId, postId string) (*model.ThreadMembership, error)
DeleteMembershipForUser(userId, postId string) error
CreateMembershipIfNeeded(userId, postId string) error
CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error)
UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error
}
type PostStore interface {

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

@@ -4157,11 +4157,11 @@ func testChannelStoreUpdateLastViewedAt(t *testing.T, ss store.Store) {
require.Nil(t, err)
var times map[string]int64
times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, m1.UserId)
times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, m1.UserId, false)
require.Nil(t, err, "failed to update ", err)
require.Equal(t, o1.LastPostAt, times[o1.Id], "last viewed at time incorrect")
times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId, m2.ChannelId}, m1.UserId)
times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId, m2.ChannelId}, m1.UserId, false)
require.Nil(t, err, "failed to update ", err)
require.Equal(t, o2.LastPostAt, times[o2.Id], "last viewed at time incorrect")
@@ -4177,7 +4177,7 @@ func testChannelStoreUpdateLastViewedAt(t *testing.T, ss store.Store) {
assert.Equal(t, o2.LastPostAt, rm2.LastUpdateAt)
assert.Equal(t, o2.TotalMsgCount, rm2.MsgCount)
_, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, "missing id")
_, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, "missing id", false)
require.Nil(t, err, "failed to update")
}
@@ -4198,16 +4198,16 @@ func testChannelStoreIncrementMentionCount(t *testing.T, ss store.Store) {
_, err := ss.Channel().SaveMember(&m1)
require.Nil(t, err)
err = ss.Channel().IncrementMentionCount(m1.ChannelId, m1.UserId)
err = ss.Channel().IncrementMentionCount(m1.ChannelId, m1.UserId, false)
require.Nil(t, err, "failed to update")
err = ss.Channel().IncrementMentionCount(m1.ChannelId, "missing id")
err = ss.Channel().IncrementMentionCount(m1.ChannelId, "missing id", false)
require.Nil(t, err, "failed to update")
err = ss.Channel().IncrementMentionCount("missing id", m1.UserId)
err = ss.Channel().IncrementMentionCount("missing id", m1.UserId, false)
require.Nil(t, err, "failed to update")
err = ss.Channel().IncrementMentionCount("missing id", "missing id")
err = ss.Channel().IncrementMentionCount("missing id", "missing id", false)
require.Nil(t, err, "failed to update")
}

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

@@ -1252,13 +1252,13 @@ func (_m *ChannelStore) GroupSyncedChannelCount() (int64, error) {
return r0, r1
}
// IncrementMentionCount provides a mock function with given fields: channelId, userId
func (_m *ChannelStore) IncrementMentionCount(channelId string, userId string) error {
ret := _m.Called(channelId, userId)
// IncrementMentionCount provides a mock function with given fields: channelId, userId, updateThreads
func (_m *ChannelStore) IncrementMentionCount(channelId string, userId string, updateThreads bool) error {
ret := _m.Called(channelId, userId, updateThreads)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(channelId, userId)
if rf, ok := ret.Get(0).(func(string, string, bool) error); ok {
r0 = rf(channelId, userId, updateThreads)
} else {
r0 = ret.Error(0)
}
@@ -1752,13 +1752,13 @@ func (_m *ChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
return r0, r1
}
// UpdateLastViewedAt provides a mock function with given fields: channelIds, userId
func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) {
ret := _m.Called(channelIds, userId)
// UpdateLastViewedAt provides a mock function with given fields: channelIds, userId, updateThreads
func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error) {
ret := _m.Called(channelIds, userId, updateThreads)
var r0 map[string]int64
if rf, ok := ret.Get(0).(func([]string, string) map[string]int64); ok {
r0 = rf(channelIds, userId)
if rf, ok := ret.Get(0).(func([]string, string, bool) map[string]int64); ok {
r0 = rf(channelIds, userId, updateThreads)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]int64)
@@ -1766,8 +1766,8 @@ func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, string) error); ok {
r1 = rf(channelIds, userId)
if rf, ok := ret.Get(1).(func([]string, string, bool) error); ok {
r1 = rf(channelIds, userId, updateThreads)
} else {
r1 = ret.Error(1)
}
@@ -1775,13 +1775,13 @@ func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (
return r0, r1
}
// UpdateLastViewedAtPost provides a mock function with given fields: unreadPost, userID, mentionCount
func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, error) {
ret := _m.Called(unreadPost, userID, mentionCount)
// UpdateLastViewedAtPost provides a mock function with given fields: unreadPost, userID, mentionCount, updateThreads
func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error) {
ret := _m.Called(unreadPost, userID, mentionCount, updateThreads)
var r0 *model.ChannelUnreadAt
if rf, ok := ret.Get(0).(func(*model.Post, string, int) *model.ChannelUnreadAt); ok {
r0 = rf(unreadPost, userID, mentionCount)
if rf, ok := ret.Get(0).(func(*model.Post, string, int, bool) *model.ChannelUnreadAt); ok {
r0 = rf(unreadPost, userID, mentionCount, updateThreads)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelUnreadAt)
@@ -1789,8 +1789,8 @@ func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID st
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Post, string, int) error); ok {
r1 = rf(unreadPost, userID, mentionCount)
if rf, ok := ret.Get(1).(func(*model.Post, string, int, bool) error); ok {
r1 = rf(unreadPost, userID, mentionCount, updateThreads)
} else {
r1 = ret.Error(1)
}

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

@@ -14,6 +14,29 @@ type ThreadStore struct {
mock.Mock
}
// CollectThreadsWithNewerReplies provides a mock function with given fields: userId, channelIds, timestamp
func (_m *ThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) {
ret := _m.Called(userId, channelIds, timestamp)
var r0 []string
if rf, ok := ret.Get(0).(func(string, []string, int64) []string); ok {
r0 = rf(userId, channelIds, timestamp)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, int64) error); ok {
r1 = rf(userId, channelIds, timestamp)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateMembershipIfNeeded provides a mock function with given fields: userId, postId
func (_m *ThreadStore) CreateMembershipIfNeeded(userId string, postId string) error {
ret := _m.Called(userId, postId)
@@ -246,3 +269,17 @@ func (_m *ThreadStore) UpdateMembership(membership *model.ThreadMembership) (*mo
return r0, r1
}
// UpdateUnreadsByChannel provides a mock function with given fields: userId, changedThreads, timestamp
func (_m *ThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error {
ret := _m.Called(userId, changedThreads, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string, int64) error); ok {
r0 = rf(userId, changedThreads, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}

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

@@ -6,8 +6,10 @@ package storetest
import (
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestThreadStore(t *testing.T, ss store.Store, s SqlSupplier) {
@@ -16,42 +18,72 @@ func TestThreadStore(t *testing.T, ss store.Store, s SqlSupplier) {
func testThreadStorePopulation(t *testing.T, ss store.Store) {
makeSomePosts := func() []*model.Post {
o1 := model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.RootId = model.NewId()
o1.Message = "zz" + model.NewId() + "b"
u1 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
u, err := ss.User().Save(&u1)
require.Nil(t, err)
c, err2 := ss.Channel().Save(&model.Channel{
DisplayName: model.NewId(),
Type: model.CHANNEL_OPEN,
Name: model.NewId(),
}, 999)
require.NoError(t, err2)
_, err44 := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
MsgCount: 90,
})
require.NoError(t, err44)
o := model.Post{}
o.ChannelId = c.Id
o.UserId = u.Id
o.Message = "zz" + model.NewId() + "b"
otmp, err3 := ss.Post().Save(&o)
require.NoError(t, err3)
o2 := model.Post{}
o2.ChannelId = model.NewId()
o2.ChannelId = c.Id
o2.UserId = model.NewId()
o2.RootId = o1.RootId
o2.RootId = otmp.Id
o2.Message = "zz" + model.NewId() + "b"
o3 := model.Post{}
o3.ChannelId = model.NewId()
o3.UserId = model.NewId()
o3.RootId = model.NewId()
o3.ChannelId = c.Id
o3.UserId = u.Id
o3.RootId = otmp.Id
o3.Message = "zz" + model.NewId() + "b"
o4 := model.Post{}
o4.ChannelId = model.NewId()
o4.ChannelId = c.Id
o4.UserId = model.NewId()
o4.Message = "zz" + model.NewId() + "b"
newPosts, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&o1, &o2, &o3, &o4})
require.Nil(t, err, "couldn't save item")
newPosts, errIdx, err3 := ss.Post().SaveMultiple([]*model.Post{&o2, &o3, &o4})
olist, _ := ss.Post().Get(otmp.Id, true)
o1 := olist.Posts[olist.Order[0]]
newPosts = append([]*model.Post{o1}, newPosts...)
require.Nil(t, err3, "couldn't save item")
require.Equal(t, -1, errIdx)
require.Len(t, newPosts, 4)
require.Equal(t, int64(2), newPosts[0].ReplyCount)
require.Equal(t, int64(2), newPosts[1].ReplyCount)
require.Equal(t, int64(1), newPosts[2].ReplyCount)
require.Equal(t, int64(2), newPosts[2].ReplyCount)
require.Equal(t, int64(0), newPosts[3].ReplyCount)
return newPosts
}
t.Run("Save replies creates a thread", func(t *testing.T) {
newPosts := makeSomePosts()
thread, err := ss.Thread().Get(newPosts[0].RootId)
thread, err := ss.Thread().Get(newPosts[0].Id)
require.Nil(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(2), thread.ReplyCount)
@@ -60,13 +92,13 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) {
o5 := model.Post{}
o5.ChannelId = model.NewId()
o5.UserId = model.NewId()
o5.RootId = newPosts[0].RootId
o5.RootId = newPosts[0].Id
o5.Message = "zz" + model.NewId() + "b"
_, _, err = ss.Post().SaveMultiple([]*model.Post{&o5})
require.Nil(t, err, "couldn't save item")
thread, err = ss.Thread().Get(newPosts[0].RootId)
thread, err = ss.Thread().Get(newPosts[0].Id)
require.Nil(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(3), thread.ReplyCount)
@@ -75,7 +107,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) {
t.Run("Delete a reply updates count on a thread", func(t *testing.T) {
newPosts := makeSomePosts()
thread, err := ss.Thread().Get(newPosts[0].RootId)
thread, err := ss.Thread().Get(newPosts[0].Id)
require.Nil(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(2), thread.ReplyCount)
@@ -84,7 +116,7 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) {
err = ss.Post().Delete(newPosts[1].Id, 1234, model.NewId())
require.Nil(t, err, "couldn't delete post")
thread, err = ss.Thread().Get(newPosts[0].RootId)
thread, err = ss.Thread().Get(newPosts[0].Id)
require.Nil(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(1), thread.ReplyCount)
@@ -197,4 +229,84 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) {
thread2, _ := ss.Thread().Get(rootPost.Id)
require.Nil(t, thread2)
})
t.Run("Thread last updated is changed when channel is updated after UpdateLastViewedAtPost", func(t *testing.T) {
newPosts := makeSomePosts()
require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id))
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.Nil(t, err1)
m.LastUpdated -= 1000
_, err := ss.Thread().UpdateMembership(m)
require.Nil(t, err)
_, err = ss.Channel().UpdateLastViewedAtPost(newPosts[0], newPosts[0].UserId, 0, true)
require.Nil(t, err)
assert.Eventually(t, func() bool {
m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.Nil(t, err2)
return m2.LastUpdated > m.LastUpdated
}, time.Second, 10*time.Millisecond)
})
t.Run("Thread last updated is changed when channel is updated after IncrementMentionCount", func(t *testing.T) {
newPosts := makeSomePosts()
require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id))
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.Nil(t, err1)
m.LastUpdated -= 1000
_, err := ss.Thread().UpdateMembership(m)
require.Nil(t, err)
err = ss.Channel().IncrementMentionCount(newPosts[0].ChannelId, newPosts[0].UserId, true)
require.Nil(t, err)
assert.Eventually(t, func() bool {
m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.Nil(t, err2)
return m2.LastUpdated > m.LastUpdated
}, time.Second, 10*time.Millisecond)
})
t.Run("Thread last updated is changed when channel is updated after UpdateLastViewedAt", func(t *testing.T) {
newPosts := makeSomePosts()
require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id))
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.Nil(t, err1)
m.LastUpdated -= 1000
_, err := ss.Thread().UpdateMembership(m)
require.Nil(t, err)
_, err = ss.Channel().UpdateLastViewedAt([]string{newPosts[0].ChannelId}, newPosts[0].UserId, true)
require.Nil(t, err)
assert.Eventually(t, func() bool {
m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.Nil(t, err2)
return m2.LastUpdated > m.LastUpdated
}, time.Second, 10*time.Millisecond)
})
t.Run("Thread last updated is changed when channel is updated after UpdateLastViewedAtPost for mark unread", func(t *testing.T) {
newPosts := makeSomePosts()
require.Nil(t, ss.Thread().CreateMembershipIfNeeded(newPosts[0].UserId, newPosts[0].Id))
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.Nil(t, err1)
m.LastUpdated += 1000
_, err := ss.Thread().UpdateMembership(m)
require.Nil(t, err)
_, err = ss.Channel().UpdateLastViewedAtPost(newPosts[0], newPosts[0].UserId, 0, true)
require.Nil(t, err)
assert.Eventually(t, func() bool {
m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.Nil(t, err2)
return m2.LastUpdated < m.LastUpdated
}, time.Second, 10*time.Millisecond)
})
}

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

@@ -2172,7 +2172,7 @@ func testUserUnreadCount(t *testing.T, ss store.Store) {
// Post one message with mention to open channel
_, nErr = ss.Post().Save(&p1)
require.Nil(t, nErr)
nErr = ss.Channel().IncrementMentionCount(c1.Id, u2.Id)
nErr = ss.Channel().IncrementMentionCount(c1.Id, u2.Id, false)
require.Nil(t, nErr)
// Post 2 messages without mention to direct channel
@@ -2183,7 +2183,7 @@ func testUserUnreadCount(t *testing.T, ss store.Store) {
_, nErr = ss.Post().Save(&p2)
require.Nil(t, nErr)
nErr = ss.Channel().IncrementMentionCount(c2.Id, u2.Id)
nErr = ss.Channel().IncrementMentionCount(c2.Id, u2.Id, false)
require.Nil(t, nErr)
p3 := model.Post{}
@@ -2193,7 +2193,7 @@ func testUserUnreadCount(t *testing.T, ss store.Store) {
_, nErr = ss.Post().Save(&p3)
require.Nil(t, nErr)
nErr = ss.Channel().IncrementMentionCount(c2.Id, u2.Id)
nErr = ss.Channel().IncrementMentionCount(c2.Id, u2.Id, false)
require.Nil(t, nErr)
badge, unreadCountErr := ss.User().GetUnreadCount(u2.Id)

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

@@ -1415,10 +1415,10 @@ func (s *TimerLayerChannelStore) GroupSyncedChannelCount() (int64, error) {
return result, err
}
func (s *TimerLayerChannelStore) IncrementMentionCount(channelId string, userId string) error {
func (s *TimerLayerChannelStore) IncrementMentionCount(channelId string, userId string, updateThreads bool) error {
start := timemodule.Now()
err := s.ChannelStore.IncrementMentionCount(channelId, userId)
err := s.ChannelStore.IncrementMentionCount(channelId, userId, updateThreads)
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
if s.Root.Metrics != nil {
@@ -1920,10 +1920,10 @@ func (s *TimerLayerChannelStore) Update(channel *model.Channel) (*model.Channel,
return result, err
}
func (s *TimerLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) {
func (s *TimerLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId string, updateThreads bool) (map[string]int64, error) {
start := timemodule.Now()
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId)
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userId, updateThreads)
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
if s.Root.Metrics != nil {
@@ -1936,10 +1936,10 @@ func (s *TimerLayerChannelStore) UpdateLastViewedAt(channelIds []string, userId
return result, err
}
func (s *TimerLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, error) {
func (s *TimerLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, updateThreads bool) (*model.ChannelUnreadAt, error) {
start := timemodule.Now()
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount)
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, updateThreads)
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
if s.Root.Metrics != nil {
@@ -6872,6 +6872,22 @@ func (s *TimerLayerTermsOfServiceStore) Save(termsOfService *model.TermsOfServic
return result, err
}
func (s *TimerLayerThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) {
start := timemodule.Now()
result, err := s.ThreadStore.CollectThreadsWithNewerReplies(userId, channelIds, timestamp)
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.CollectThreadsWithNewerReplies", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) CreateMembershipIfNeeded(userId string, postId string) error {
start := timemodule.Now()
@@ -7048,6 +7064,22 @@ func (s *TimerLayerThreadStore) UpdateMembership(membership *model.ThreadMembers
return result, err
}
func (s *TimerLayerThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64) error {
start := timemodule.Now()
err := s.ThreadStore.UpdateUnreadsByChannel(userId, changedThreads, timestamp)
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.UpdateUnreadsByChannel", success, elapsed)
}
return err
}
func (s *TimerLayerTokenStore) Cleanup() {
start := timemodule.Now()

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

@@ -55,6 +55,8 @@ func TestMfaRequired(t *testing.T) {
th.App.SetSession(&model.Session{Id: "abc", UserId: "userid"})
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AnnouncementSettings.UserNoticesEnabled = false
*cfg.AnnouncementSettings.AdminNoticesEnabled = false
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
*cfg.ServiceSettings.EnforceMultifactorAuthentication = true
})