[MM-56904] Reduce the number of api requests made to fetch user information for GMs on page load (#27149)

* use new endpoint to fetch group members
This commit is contained in:
Ben Cooke 2024-07-25 15:57:23 -04:00 committed by GitHub
parent d89ffe269f
commit b244bb621d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 516 additions and 76 deletions

View file

@ -933,3 +933,89 @@ func TestDeleteSidebarPreferences(t *testing.T) {
assert.NotContains(t, categories.Categories[1].Channels, channel.Id)
})
}
func TestUpdateLimitVisibleDMsGMs(t *testing.T) {
t.Run("Update limit_visible_dms_gms to a valid value", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.LoginBasic()
user := th.BasicUser
_, err := client.UpdatePreferences(context.Background(), user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategorySidebarSettings,
Name: model.PreferenceLimitVisibleDmsGms,
Value: "40",
},
})
require.NoError(t, err)
pref, _, err := client.GetPreferenceByCategoryAndName(context.Background(), user.Id, model.PreferenceCategorySidebarSettings, model.PreferenceLimitVisibleDmsGms)
require.NoError(t, err)
require.Equal(t, "40", pref.Value, "Value was not updated")
})
t.Run("Update limit_visible_dms_gms to a value greater PreferenceMaxLimitVisibleDmsGmsValue", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.LoginBasic()
user := th.BasicUser
resp, err := client.UpdatePreferences(context.Background(), user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategorySidebarSettings,
Name: model.PreferenceLimitVisibleDmsGms,
Value: "10000",
},
})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("Update limit_visible_dms_gms to an invalid value", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.LoginBasic()
user := th.BasicUser
resp, err := client.UpdatePreferences(context.Background(), user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategorySidebarSettings,
Name: model.PreferenceLimitVisibleDmsGms,
Value: "one thousand",
},
})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("Update limit_visible_dms_gms to a negative number", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.LoginBasic()
user := th.BasicUser
resp, err := client.UpdatePreferences(context.Background(), user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategorySidebarSettings,
Name: model.PreferenceLimitVisibleDmsGms,
Value: "-20",
},
})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
}

View file

@ -646,6 +646,27 @@ func (s *Server) doDeleteOrphanDraftsMigration(c request.CTX) error {
return nil
}
func (s *Server) doDeleteDmsPreferencesMigration(c request.CTX) error {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(model.MigrationKeyDeleteDmsPreferences); err == nil {
return nil
}
jobs, err := s.Store().Job().GetAllByTypeAndStatus(c, model.JobTypeDeleteDmsPreferencesMigration, model.JobStatusPending)
if err != nil {
return fmt.Errorf("failed to get jobs by type and status: %w", err)
}
if len(jobs) > 0 {
return nil
}
if _, appErr := s.Jobs.CreateJobOnce(c, model.JobTypeDeleteDmsPreferencesMigration, nil); appErr != nil {
return fmt.Errorf("failed to start job for deleting dm preferences: %w", appErr)
}
return nil
}
func (a *App) DoAppMigrations() {
a.Srv().doAppMigrations()
}
@ -688,6 +709,7 @@ func (s *Server) doAppMigrations() {
{"Encode S3 Image Paths Migration", s.doCloudS3PathMigrations},
{"Delete Empty Drafts Migration", s.doDeleteEmptyDraftsMigration},
{"Delete Orphan Drafts Migration", s.doDeleteOrphanDraftsMigration},
{"Delete Invalid Dms Preferences Migration", s.doDeleteDmsPreferencesMigration},
}
c := request.EmptyContext(s.Log())

View file

@ -41,6 +41,7 @@ import (
"github.com/mattermost/mattermost/server/v8/channels/jobs"
"github.com/mattermost/mattermost/server/v8/channels/jobs/active_users"
"github.com/mattermost/mattermost/server/v8/channels/jobs/cleanup_desktop_tokens"
"github.com/mattermost/mattermost/server/v8/channels/jobs/delete_dms_preferences_migration"
"github.com/mattermost/mattermost/server/v8/channels/jobs/delete_empty_drafts_migration"
"github.com/mattermost/mattermost/server/v8/channels/jobs/delete_orphan_drafts_migration"
"github.com/mattermost/mattermost/server/v8/channels/jobs/expirynotify"
@ -1603,6 +1604,11 @@ func (s *Server) initJobs() {
nil,
)
s.Jobs.RegisterJobType(
model.JobTypeDeleteDmsPreferencesMigration,
delete_dms_preferences_migration.MakeWorker(s.Jobs, s.Store(), New(ServerConnector(s.Channels()))),
nil)
s.platform.Jobs = s.Jobs
}

View file

@ -0,0 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package delete_dms_preferences_migration
import (
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/jobs"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/pkg/errors"
)
const (
timeBetweenBatches = 1 * time.Second
)
// MakeWorker creates a batch migration worker to delete empty drafts.
func MakeWorker(jobServer *jobs.JobServer, store store.Store, app jobs.BatchMigrationWorkerAppIFace) model.Worker {
return jobs.MakeBatchMigrationWorker(
jobServer,
store,
app,
model.MigrationKeyDeleteDmsPreferences,
timeBetweenBatches,
doDeleteDmsPreferencesMigrationBatch,
)
}
// doDeleteDmsPreferencesMigrationBatch deletes any limit_visible_dms_gms preferences with a value > 40 and less than 1.
func doDeleteDmsPreferencesMigrationBatch(data model.StringMap, store store.Store) (model.StringMap, bool, error) {
rowAffected, err := store.Preference().DeleteInvalidVisibleDmsGms()
if err != nil {
return nil, false, errors.Wrapf(err, "failed to delete invalid limit_visible_dms_gms")
}
if rowAffected == 0 {
return nil, true, nil
}
return nil, false, nil
}

View file

@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package delete_dms_preferences_migration
import (
"errors"
"testing"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDoDeleteDMsPreferencesMigrationBatch(t *testing.T) {
t.Run("failure deleting batch", func(t *testing.T) {
mockStore := &storetest.Store{}
t.Cleanup(func() {
mockStore.AssertExpectations(t)
})
mockStore.PreferenceStore.On("DeleteInvalidVisibleDmsGms").Return(int64(0), errors.New("failure"))
data, done, err := doDeleteDmsPreferencesMigrationBatch(nil, mockStore)
require.EqualError(t, err, "failed to delete invalid limit_visible_dms_gms: failure")
assert.False(t, done)
assert.Nil(t, data)
})
t.Run("do batches", func(t *testing.T) {
mockStore := &storetest.Store{}
t.Cleanup(func() {
mockStore.AssertExpectations(t)
})
mockStore.PreferenceStore.On("DeleteInvalidVisibleDmsGms").Return(int64(10), nil)
data, done, err := doDeleteDmsPreferencesMigrationBatch(nil, mockStore)
require.NoError(t, err)
assert.False(t, done)
assert.Nil(t, data)
})
t.Run("done batches", func(t *testing.T) {
mockStore := &storetest.Store{}
t.Cleanup(func() {
mockStore.AssertExpectations(t)
})
mockStore.PreferenceStore.On("DeleteInvalidVisibleDmsGms").Return(int64(0), nil)
data, done, err := doDeleteDmsPreferencesMigrationBatch(nil, mockStore)
require.NoError(t, err)
assert.True(t, done)
assert.Nil(t, data)
})
}

View file

@ -7378,6 +7378,24 @@ func (s *OpenTracingLayerPreferenceStore) DeleteCategoryAndName(category string,
return err
}
func (s *OpenTracingLayerPreferenceStore) DeleteInvalidVisibleDmsGms() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.DeleteInvalidVisibleDmsGms")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.DeleteInvalidVisibleDmsGms()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.DeleteOrphanedRows")

View file

@ -8384,6 +8384,27 @@ func (s *RetryLayerPreferenceStore) DeleteCategoryAndName(category string, name
}
func (s *RetryLayerPreferenceStore) DeleteInvalidVisibleDmsGms() (int64, error) {
tries := 0
for {
result, err := s.PreferenceStore.DeleteInvalidVisibleDmsGms()
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
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0

View file

@ -315,3 +315,54 @@ func (s SqlPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) {
return rowsAffected, nil
}
// Delete preference for limit_visible_dms_gms where their value is greater than "40" or less than "1"
func (s SqlPreferenceStore) DeleteInvalidVisibleDmsGms() (int64, error) {
var queryString string
var args []interface{}
var err error
// We need to pad the value field with zeros when doing comparison's because the value is stored as a string.
// Having them the same length allows Postgres/MySQL to compare them correctly.
whereClause := sq.And{
sq.Eq{"Category": model.PreferenceCategorySidebarSettings},
sq.Eq{"Name": model.PreferenceLimitVisibleDmsGms},
sq.Or{
sq.Gt{"SUBSTRING(CONCAT('000000000000000', Value), LENGTH(Value) + 1, 15)": "000000000000040"},
sq.Lt{"SUBSTRING(CONCAT('000000000000000', Value), LENGTH(Value) + 1, 15)": "000000000000001"},
},
}
if s.DriverName() == "postgres" {
subQuery := s.getQueryBuilder().
Select("UserId, Category, Name").
From("Preferences").
Where(whereClause).
Limit(100)
queryString, args, err = s.getQueryBuilder().
Delete("Preferences").
Where(sq.Expr("(userid, category, name) IN (?)", subQuery)).
ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "could not build sql query to delete preference")
}
} else {
queryString, args, err = s.getQueryBuilder().
Delete("Preferences").
Where(whereClause).
Limit(100).
ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "could not build sql query to delete preference")
}
}
result, err := s.GetMasterX().Exec(queryString, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to delete Preference")
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "unable to get rows affected")
}
return rowsAffected, nil
}

View file

@ -15,7 +15,7 @@ import (
)
func TestPreferenceStore(t *testing.T) {
StoreTest(t, storetest.TestPreferenceStore)
StoreTestWithSqlStore(t, storetest.TestPreferenceStore)
}
func TestDeleteUnusedFeatures(t *testing.T) {

View file

@ -652,6 +652,7 @@ type PreferenceStore interface {
PermanentDeleteByUser(userID string) error
DeleteOrphanedRows(limit int) (deleted int64, err error)
CleanupFlagsBatch(limit int64) (int64, error)
DeleteInvalidVisibleDmsGms() (int64, error)
}
type LicenseStore interface {

View file

@ -96,6 +96,34 @@ func (_m *PreferenceStore) DeleteCategoryAndName(category string, name string) e
return r0
}
// DeleteInvalidVisibleDmsGms provides a mock function with given fields:
func (_m *PreferenceStore) DeleteInvalidVisibleDmsGms() (int64, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for DeleteInvalidVisibleDmsGms")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func() (int64, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *PreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)

View file

@ -14,7 +14,7 @@ import (
"github.com/mattermost/mattermost/server/v8/channels/store"
)
func TestPreferenceStore(t *testing.T, rctx request.CTX, ss store.Store) {
func TestPreferenceStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("PreferenceSave", func(t *testing.T) { testPreferenceSave(t, rctx, ss) })
t.Run("PreferenceGet", func(t *testing.T) { testPreferenceGet(t, rctx, ss) })
t.Run("PreferenceGetCategory", func(t *testing.T) { testPreferenceGetCategory(t, rctx, ss) })
@ -24,6 +24,7 @@ func TestPreferenceStore(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("PreferenceDeleteCategory", func(t *testing.T) { testPreferenceDeleteCategory(t, rctx, ss) })
t.Run("PreferenceDeleteCategoryAndName", func(t *testing.T) { testPreferenceDeleteCategoryAndName(t, rctx, ss) })
t.Run("PreferenceDeleteOrphanedRows", func(t *testing.T) { testPreferenceDeleteOrphanedRows(t, rctx, ss) })
t.Run("PreferenceDeleteInvalidVisibleDmsGms", func(t *testing.T) { testDeleteInvalidVisibleDmsGms(t, rctx, ss, s) })
}
func testPreferenceSave(t *testing.T, rctx request.CTX, ss store.Store) {
@ -401,3 +402,78 @@ func testPreferenceDeleteOrphanedRows(t *testing.T, rctx request.CTX, ss store.S
_, nErr = ss.Preference().Get(userId, category, preference2.Name)
assert.NoError(t, nErr, "newer preference should not have been deleted")
}
func testDeleteInvalidVisibleDmsGms(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
userId1 := model.NewId()
userId2 := model.NewId()
userId3 := model.NewId()
userId4 := model.NewId()
category := model.PreferenceCategorySidebarSettings
name := model.PreferenceLimitVisibleDmsGms
preferences := model.Preferences{
{
UserId: userId1,
Category: category,
Name: name,
Value: "10000",
},
{
UserId: userId2,
Category: category,
Name: name,
Value: "40",
},
{
UserId: userId3,
Category: category,
Name: name,
Value: "invalid",
},
{
UserId: model.NewId(),
Category: category,
Name: name,
Value: "-10",
},
{
UserId: model.NewId(),
Category: category,
Name: name,
Value: "0",
},
{
UserId: model.NewId(),
Category: category,
Name: name,
Value: "00000",
},
{
UserId: userId4,
Category: category,
Name: name,
Value: "20",
},
}
// Can't insert with Save methods because the values are invalid
_, execerr := s.GetMasterX().NamedExec(`
INSERT INTO
Preferences(UserId, Category, Name, Value)
VALUES
(:UserId, :Category, :Name, :Value);
`, preferences)
require.NoError(t, execerr)
count, err := ss.Preference().DeleteInvalidVisibleDmsGms()
require.NoError(t, err)
assert.Equal(t, int64(5), count)
preference, err := ss.Preference().Get(userId2, category, name)
require.NoError(t, err)
require.Equal(t, &preferences[1], preference)
preference, err = ss.Preference().Get(userId4, category, name)
require.NoError(t, err)
require.Equal(t, &preferences[6], preference)
}

View file

@ -6665,6 +6665,22 @@ func (s *TimerLayerPreferenceStore) DeleteCategoryAndName(category string, name
return err
}
func (s *TimerLayerPreferenceStore) DeleteInvalidVisibleDmsGms() (int64, error) {
start := time.Now()
result, err := s.PreferenceStore.DeleteInvalidVisibleDmsGms()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.DeleteInvalidVisibleDmsGms", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()

View file

@ -76,6 +76,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
systemStore.On("GetByName", model.MigrationKeyAddIPFilteringPermissions).Return(&model.System{Name: model.MigrationKeyAddIPFilteringPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddOutgoingOAuthConnectionsPermissions).Return(&model.System{Name: model.MigrationKeyAddOutgoingOAuthConnectionsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddChannelBookmarksPermissions).Return(&model.System{Name: model.MigrationKeyAddChannelBookmarksPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyDeleteDmsPreferences).Return(&model.System{Name: model.MigrationKeyDeleteDmsPreferences, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddManageJobAncillaryPermissions).Return(&model.System{Name: model.MigrationKeyAddManageJobAncillaryPermissions, Value: "true"}, nil)
systemStore.On("GetByName", "CustomGroupAdminRoleCreationMigrationComplete").Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
systemStore.On("GetByName", "products_boards").Return(&model.System{Name: "products_boards", Value: "true"}, nil)

View file

@ -9486,6 +9486,10 @@
"id": "model.preference.is_valid.id.app_error",
"translation": "Invalid user id."
},
{
"id": "model.preference.is_valid.limit_visible_dms_gms.app_error",
"translation": "Invalid value for limit_visible_dms_gms."
},
{
"id": "model.preference.is_valid.name.app_error",
"translation": "Invalid name."

View file

@ -8,37 +8,38 @@ import (
)
const (
JobTypeDataRetention = "data_retention"
JobTypeMessageExport = "message_export"
JobTypeElasticsearchPostIndexing = "elasticsearch_post_indexing"
JobTypeElasticsearchPostAggregation = "elasticsearch_post_aggregation"
JobTypeBlevePostIndexing = "bleve_post_indexing"
JobTypeLdapSync = "ldap_sync"
JobTypeMigrations = "migrations"
JobTypePlugins = "plugins"
JobTypeExpiryNotify = "expiry_notify"
JobTypeProductNotices = "product_notices"
JobTypeActiveUsers = "active_users"
JobTypeImportProcess = "import_process"
JobTypeImportDelete = "import_delete"
JobTypeExportProcess = "export_process"
JobTypeExportDelete = "export_delete"
JobTypeCloud = "cloud"
JobTypeResendInvitationEmail = "resend_invitation_email"
JobTypeExtractContent = "extract_content"
JobTypeLastAccessiblePost = "last_accessible_post"
JobTypeLastAccessibleFile = "last_accessible_file"
JobTypeUpgradeNotifyAdmin = "upgrade_notify_admin"
JobTypeTrialNotifyAdmin = "trial_notify_admin"
JobTypePostPersistentNotifications = "post_persistent_notifications"
JobTypeInstallPluginNotifyAdmin = "install_plugin_notify_admin"
JobTypeHostedPurchaseScreening = "hosted_purchase_screening"
JobTypeS3PathMigration = "s3_path_migration"
JobTypeCleanupDesktopTokens = "cleanup_desktop_tokens"
JobTypeDeleteEmptyDraftsMigration = "delete_empty_drafts_migration"
JobTypeRefreshPostStats = "refresh_post_stats"
JobTypeDeleteOrphanDraftsMigration = "delete_orphan_drafts_migration"
JobTypeExportUsersToCSV = "export_users_to_csv"
JobTypeDataRetention = "data_retention"
JobTypeMessageExport = "message_export"
JobTypeElasticsearchPostIndexing = "elasticsearch_post_indexing"
JobTypeElasticsearchPostAggregation = "elasticsearch_post_aggregation"
JobTypeBlevePostIndexing = "bleve_post_indexing"
JobTypeLdapSync = "ldap_sync"
JobTypeMigrations = "migrations"
JobTypePlugins = "plugins"
JobTypeExpiryNotify = "expiry_notify"
JobTypeProductNotices = "product_notices"
JobTypeActiveUsers = "active_users"
JobTypeImportProcess = "import_process"
JobTypeImportDelete = "import_delete"
JobTypeExportProcess = "export_process"
JobTypeExportDelete = "export_delete"
JobTypeCloud = "cloud"
JobTypeResendInvitationEmail = "resend_invitation_email"
JobTypeExtractContent = "extract_content"
JobTypeLastAccessiblePost = "last_accessible_post"
JobTypeLastAccessibleFile = "last_accessible_file"
JobTypeUpgradeNotifyAdmin = "upgrade_notify_admin"
JobTypeTrialNotifyAdmin = "trial_notify_admin"
JobTypePostPersistentNotifications = "post_persistent_notifications"
JobTypeInstallPluginNotifyAdmin = "install_plugin_notify_admin"
JobTypeHostedPurchaseScreening = "hosted_purchase_screening"
JobTypeS3PathMigration = "s3_path_migration"
JobTypeCleanupDesktopTokens = "cleanup_desktop_tokens"
JobTypeDeleteEmptyDraftsMigration = "delete_empty_drafts_migration"
JobTypeRefreshPostStats = "refresh_post_stats"
JobTypeDeleteOrphanDraftsMigration = "delete_orphan_drafts_migration"
JobTypeExportUsersToCSV = "export_users_to_csv"
JobTypeDeleteDmsPreferencesMigration = "delete_dms_preferences_migration"
JobStatusPending = "pending"
JobStatusInProgress = "in_progress"

View file

@ -47,5 +47,6 @@ const (
MigrationKeyAddIPFilteringPermissions = "add_ip_filtering_permissions"
MigrationKeyAddOutgoingOAuthConnectionsPermissions = "add_outgoing_oauth_connections_permissions"
MigrationKeyAddChannelBookmarksPermissions = "add_channel_bookmarks_permissions"
MigrationKeyDeleteDmsPreferences = "delete_dms_preferences_migration"
MigrationKeyAddManageJobAncillaryPermissions = "add_manage_jobs_ancillary_permissions"
)

View file

@ -7,6 +7,7 @@ import (
"encoding/json"
"net/http"
"regexp"
"strconv"
"strings"
"unicode/utf8"
)
@ -61,7 +62,9 @@ const (
PreferenceEmailIntervalHourAsSeconds = "3600"
PreferenceCloudUserEphemeralInfo = "cloud_user_ephemeral_info"
MaxPreferenceValueLength = 20000
PreferenceLimitVisibleDmsGms = "limit_visible_dms_gms"
PreferenceMaxLimitVisibleDmsGmsValue = 40
MaxPreferenceValueLength = 20000
)
type Preference struct {
@ -97,6 +100,13 @@ func (o *Preference) IsValid() *AppError {
}
}
if o.Category == PreferenceCategorySidebarSettings && o.Name == PreferenceLimitVisibleDmsGms {
visibleDmsGmsValue, convErr := strconv.Atoi(o.Value)
if convErr != nil || visibleDmsGmsValue < 1 || visibleDmsGmsValue > PreferenceMaxLimitVisibleDmsGmsValue {
return NewAppError("Preference.IsValid", "model.preference.is_valid.limit_visible_dms_gms.app_error", nil, "value="+o.Value, http.StatusBadRequest)
}
}
return nil
}

View file

@ -70,6 +70,34 @@ func TestPreferenceIsValid(t *testing.T) {
require.Nil(t, preference.IsValid())
})
t.Run("limit_visible_dms_gms has a valid value", func(t *testing.T) {
preference.Category = PreferenceCategorySidebarSettings
preference.Name = PreferenceLimitVisibleDmsGms
preference.Value = "40"
require.Nil(t, preference.IsValid())
})
t.Run("limit_visible_dms_gms has a value greater than PreferenceMaxLimitVisibleDmsGmsValue", func(t *testing.T) {
preference.Category = PreferenceCategorySidebarSettings
preference.Name = PreferenceLimitVisibleDmsGms
preference.Value = "10000"
require.NotNil(t, preference.IsValid())
})
t.Run("limit_visible_dms_gms has an invalid value", func(t *testing.T) {
preference.Category = PreferenceCategorySidebarSettings
preference.Name = PreferenceLimitVisibleDmsGms
preference.Value = "one thousand"
require.NotNil(t, preference.IsValid())
})
t.Run("limit_visible_dms_gms has a negative number", func(t *testing.T) {
preference.Category = PreferenceCategorySidebarSettings
preference.Name = PreferenceLimitVisibleDmsGms
preference.Value = "-10"
require.NotNil(t, preference.IsValid())
})
}
func TestPreferencePreUpdate(t *testing.T) {

View file

@ -520,11 +520,8 @@ describe('Actions.User', () => {
});
});
test('Should call p-queue APIs on loadProfilesForGM', async () => {
test('Should call getProfilesInGroupChannels on loadProfilesForGM', async () => {
const gmChannel = {id: 'gmChannel', type: General.GM_CHANNEL, team_id: '', delete_at: 0};
UserActions.queue.add = jest.fn().mockReturnValue(jest.fn());
UserActions.queue.onEmpty = jest.fn();
const user = TestHelper.fakeUser();
const profiles = {
@ -555,7 +552,7 @@ describe('Actions.User', () => {
profiles,
statuses: {},
profilesInChannel: {
[gmChannel.id]: new Set(['current_user_id']),
[gmChannel.id]: new Set([]),
},
},
teams: {
@ -605,9 +602,10 @@ describe('Actions.User', () => {
const testStore = mockStore(state);
store.getState.mockImplementation(testStore.getState);
store.dispatch.mockImplementation(testStore.dispatch);
const actions = testStore.getActions();
await UserActions.loadProfilesForGM();
expect(UserActions.queue.onEmpty).toHaveBeenCalled();
expect(UserActions.queue.add).toHaveBeenCalled();
expect(actions).toEqual([{args: [['gmChannel']], type: 'MOCK_GET_PROFILES_IN_GROUP_CHANNELS'}]);
});
});

View file

@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PQueue from 'p-queue';
import type {UserAutocomplete} from '@mattermost/types/autocomplete';
import type {Channel} from '@mattermost/types/channels';
import type {UserProfile, UserStatus} from '@mattermost/types/users';
@ -37,7 +35,6 @@ import * as Utils from 'utils/utils';
import type {GlobalState} from 'types/store';
export const queue = new PQueue({concurrency: 4});
const dispatch = store.dispatch;
const getState = store.getState;
@ -312,9 +309,9 @@ export async function loadProfilesForGM() {
const collapsedThreads = isCollapsedThreadsEnabled(state);
const userIdsForLoadingCustomEmojis = new Set();
const channelUsersToLoad: string[] = [];
for (const channel of getGMsForLoading(state)) {
const userIds = userIdsInChannels[channel.id] || new Set();
userIds.forEach((userId) => userIdsForLoadingCustomEmojis.add(userId));
if (userIds.size >= Constants.MIN_USERS_IN_GM) {
@ -340,12 +337,14 @@ export async function loadProfilesForGM() {
value: 'true',
});
}
const getProfilesAction = UserActions.getProfilesInChannel(channel.id, 0, Constants.MAX_USERS_IN_GM);
queue.add(() => dispatch(getProfilesAction));
if (userIds.size === 0) {
channelUsersToLoad.push(channel.id);
}
}
await queue.onEmpty();
if (channelUsersToLoad.length > 0) {
await dispatch(UserActions.getProfilesInGroupChannels(channelUsersToLoad));
}
if (userIdsForLoadingCustomEmojis.size > 0) {
dispatch(loadCustomEmojisForCustomStatusesByUserIds(userIdsForLoadingCustomEmojis));

View file

@ -102,17 +102,6 @@ exports[`components/sidebar/sidebar_category/sidebar_category_sorting_menu shoul
</React.Fragment>
}
>
<MenuItem
id="showAllDms-category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="All direct messages"
id="sidebar.allDirectMessages"
/>
}
onClick={[Function]}
/>
<MenuItemSeparator />
<MenuItem
id="showDmCount-category_id-10"
key="showDmCount-category_id-10"

View file

@ -147,17 +147,6 @@ const SidebarCategorySortingMenu = ({
)}
menuId={`showMessagesCount-${category.id}-menu`}
>
<Menu.Item
id={`showAllDms-${category.id}`}
labels={(
<FormattedMessage
id='sidebar.allDirectMessages'
defaultMessage='All direct messages'
/>
)}
onClick={() => handlelimitVisibleDMsGMs(Constants.HIGHEST_DM_SHOW_COUNT)}
/>
<Menu.Separator/>
{Constants.DM_AND_GM_SHOW_COUNTS.map((dmGmShowCount) => (
<Menu.Item
id={`showDmCount-${category.id}-${dmGmShowCount}`}

View file

@ -16,8 +16,6 @@ import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import {localizeMessage} from 'utils/utils';
type Limit = {
value: number;
label: string;
@ -44,7 +42,6 @@ type State = {
}
const limits: Limit[] = [
{value: 10000, label: localizeMessage('user.settings.sidebar.limitVisibleGMsDMs.allDirectMessages', 'All Direct Messages')},
{value: 10, label: '10'},
{value: 15, label: '15'},
{value: 20, label: '20'},

View file

@ -4972,7 +4972,6 @@
"sidebar_right_menu.console": "System Console",
"sidebar_right_menu.flagged": "Saved messages",
"sidebar_right_menu.recentMentions": "Recent Mentions",
"sidebar.allDirectMessages": "All direct messages",
"sidebar.createDirectMessage": "Write a direct message",
"sidebar.createUserGroup": "Create New User Group",
"sidebar.directchannel.you": "{displayname} (you)",
@ -5663,7 +5662,6 @@
"user.settings.security.viewHistory": "View Access History",
"user.settings.security.viewHistory.icon": "Access History Icon",
"user.settings.sidebar.icon": "Sidebar Settings Icon",
"user.settings.sidebar.limitVisibleGMsDMs.allDirectMessages": "All Direct Messages",
"user.settings.sidebar.limitVisibleGMsDMsDesc": "You can also change these settings in the direct messages sidebar menu.",
"user.settings.sidebar.limitVisibleGMsDMsTitle": "Number of direct messages to show",
"user.settings.sidebar.off": "Off",