diff --git a/server/channels/api4/resolver.go b/server/channels/api4/resolver.go index 2e46e6d8b22..1ebbcf04dd7 100644 --- a/server/channels/api4/resolver.go +++ b/server/channels/api4/resolver.go @@ -353,9 +353,12 @@ func (*resolver) SidebarCategories(ctx context.Context, args struct { return nil, appErr } } else { + appsCategoryEnabled := c.App.Config().FeatureFlags.AppsSidebarCategory + opts := &store.SidebarCategorySearchOpts{ - TeamID: args.TeamID, - ExcludeTeam: args.ExcludeTeam, + TeamID: args.TeamID, + ExcludeTeam: args.ExcludeTeam, + AppsCategoryEnabled: appsCategoryEnabled, } categories, appErr = c.App.GetSidebarCategories(c.AppContext, args.UserID, opts) if appErr != nil { diff --git a/server/channels/app/channel_category.go b/server/channels/app/channel_category.go index 6b4ce964d28..4c8e3585bb3 100644 --- a/server/channels/app/channel_category.go +++ b/server/channels/app/channel_category.go @@ -25,13 +25,17 @@ func (a *App) createInitialSidebarCategories(userID string, opts *store.SidebarC func (a *App) GetSidebarCategoriesForTeamForUser(c request.CTX, userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) { var appErr *model.AppError - categories, err := a.Srv().Store().Channel().GetSidebarCategoriesForTeamForUser(userID, teamID) - if err == nil && len(categories.Categories) == 0 { - // A user must always have categories, so migration must not have happened yet, and we should run it ourselves - categories, appErr = a.createInitialSidebarCategories(userID, &store.SidebarCategorySearchOpts{ - TeamID: teamID, - ExcludeTeam: false, - }) + appsCategoryEnabled := a.Config().FeatureFlags.AppsSidebarCategory + options := &store.SidebarCategorySearchOpts{ + TeamID: teamID, + ExcludeTeam: false, + AppsCategoryEnabled: appsCategoryEnabled, + } + categories, err := a.Srv().Store().Channel().GetSidebarCategoriesForTeamForUser(userID, teamID, options) + if err == nil && (len(categories.Categories) == 0 || (appsCategoryEnabled && checkMissingSystemSidebarCategories(categories))) { + // A user must always have system categories, so migration must not have happened yet, and we should run it ourselves + categories, appErr = a.createInitialSidebarCategories(userID, options) + if appErr != nil { return nil, appErr } @@ -52,10 +56,17 @@ func (a *App) GetSidebarCategoriesForTeamForUser(c request.CTX, userID, teamID s func (a *App) GetSidebarCategories(c request.CTX, userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, *model.AppError) { var appErr *model.AppError - categories, err := a.Srv().Store().Channel().GetSidebarCategories(userID, opts) - if err == nil && len(categories.Categories) == 0 { - // A user must always have categories, so migration must not have happened yet, and we should run it ourselves - categories, appErr = a.createInitialSidebarCategories(userID, opts) + appsCategoryEnabled := a.Config().FeatureFlags.AppsSidebarCategory + options := &store.SidebarCategorySearchOpts{ + TeamID: opts.TeamID, + ExcludeTeam: opts.ExcludeTeam, + AppsCategoryEnabled: appsCategoryEnabled, + } + categories, err := a.Srv().Store().Channel().GetSidebarCategories(userID, options) + if err == nil && (len(categories.Categories) == 0 || (appsCategoryEnabled && checkMissingSystemSidebarCategories(categories))) { + // A user must always have system categories, so migration must not have happened yet, and we should run it ourselves + categories, appErr = a.createInitialSidebarCategories(userID, options) + if appErr != nil { return nil, appErr } @@ -90,7 +101,8 @@ func (a *App) GetSidebarCategoryOrder(c request.CTX, userID, teamID string) ([]s } func (a *App) GetSidebarCategory(c request.CTX, categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) { - category, err := a.Srv().Store().Channel().GetSidebarCategory(categoryId) + appsCategoryEnabled := a.Config().FeatureFlags.AppsSidebarCategory + category, err := a.Srv().Store().Channel().GetSidebarCategory(categoryId, &store.SidebarCategorySearchOpts{AppsCategoryEnabled: appsCategoryEnabled}) if err != nil { var nfErr *store.ErrNotFound switch { @@ -105,7 +117,8 @@ func (a *App) GetSidebarCategory(c request.CTX, categoryId string) (*model.Sideb } func (a *App) CreateSidebarCategory(c request.CTX, userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { - category, err := a.Srv().Store().Channel().CreateSidebarCategory(userID, teamID, newCategory) + appsCategoryEnabled := a.Config().FeatureFlags.AppsSidebarCategory + category, err := a.Srv().Store().Channel().CreateSidebarCategory(userID, teamID, newCategory, &store.SidebarCategorySearchOpts{AppsCategoryEnabled: appsCategoryEnabled}) if err != nil { var nfErr *store.ErrNotFound switch { @@ -142,7 +155,13 @@ func (a *App) UpdateSidebarCategoryOrder(c request.CTX, userID, teamID string, c } func (a *App) UpdateSidebarCategories(c request.CTX, userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) { - updatedCategories, originalCategories, err := a.Srv().Store().Channel().UpdateSidebarCategories(userID, teamID, categories) + appsCategoryEnabled := a.Config().FeatureFlags.AppsSidebarCategory + updatedCategories, originalCategories, err := a.Srv().Store().Channel().UpdateSidebarCategories( + userID, + teamID, + categories, + &store.SidebarCategorySearchOpts{AppsCategoryEnabled: appsCategoryEnabled}, + ) if err != nil { return nil, model.NewAppError("UpdateSidebarCategories", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -286,3 +305,17 @@ func (a *App) DeleteSidebarCategory(c request.CTX, userID, teamID, categoryId st return nil } + +func checkMissingSystemSidebarCategories(categories *model.OrderedSidebarCategories) bool { + missingSystemCategories := make(map[model.SidebarCategoryType]struct{}) + + for _, systemSidebarCategory := range model.SystemSidebarCategories { + missingSystemCategories[systemSidebarCategory] = struct{}{} + } + + for _, category := range categories.Categories { + delete(missingSystemCategories, category.Type) + } + + return len(missingSystemCategories) != 0 +} diff --git a/server/channels/app/channel_category_with_apps_category_test.go b/server/channels/app/channel_category_with_apps_category_test.go new file mode 100644 index 00000000000..7ce2f9fbb34 --- /dev/null +++ b/server/channels/app/channel_category_with_apps_category_test.go @@ -0,0 +1,481 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/server/v8/model" +) + +func TestSidebarCategoryWithAppsCategory(t *testing.T) { + th := Setup(t).InitBasicWithAppsSidebarEnabled() + defer th.TearDown() + + basicChannel2 := th.CreateChannel(th.Context, th.BasicTeam) + defer th.App.PermanentDeleteChannel(th.Context, basicChannel2) + user := th.CreateUser() + defer th.App.Srv().Store().User().PermanentDelete(user.Id) + th.LinkUserToTeam(user, th.BasicTeam) + th.AddUserToChannel(user, basicChannel2) + + var createdCategory *model.SidebarCategoryWithChannels + t.Run("CreateSidebarCategory", func(t *testing.T) { + catData := model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "TEST", + }, + Channels: []string{th.BasicChannel.Id, basicChannel2.Id, basicChannel2.Id}, + } + _, err := th.App.CreateSidebarCategory(th.Context, user.Id, th.BasicTeam.Id, &catData) + require.NotNil(t, err, "Should return error due to duplicate IDs") + catData.Channels = []string{th.BasicChannel.Id, basicChannel2.Id} + cat, err := th.App.CreateSidebarCategory(th.Context, user.Id, th.BasicTeam.Id, &catData) + require.Nil(t, err, "Expected no error") + require.NotNil(t, cat, "Expected category object, got nil") + createdCategory = cat + }) + + t.Run("UpdateSidebarCategories", func(t *testing.T) { + require.NotNil(t, createdCategory) + createdCategory.Channels = []string{th.BasicChannel.Id} + updatedCat, err := th.App.UpdateSidebarCategories(th.Context, user.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{createdCategory}) + require.Nil(t, err, "Expected no error") + require.NotNil(t, updatedCat, "Expected category object, got nil") + require.Len(t, updatedCat, 1) + require.Len(t, updatedCat[0].Channels, 1) + require.Equal(t, updatedCat[0].Channels[0], th.BasicChannel.Id) + }) + + t.Run("UpdateSidebarCategoryOrder", func(t *testing.T) { + err := th.App.UpdateSidebarCategoryOrder(th.Context, user.Id, th.BasicTeam.Id, []string{th.BasicChannel.Id, basicChannel2.Id}) + require.NotNil(t, err, "Should return error due to invalid order") + + actualOrder, err := th.App.GetSidebarCategoryOrder(th.Context, user.Id, th.BasicTeam.Id) + require.Nil(t, err, "Should fetch order successfully") + + actualOrder[2], actualOrder[3] = actualOrder[3], actualOrder[2] + err = th.App.UpdateSidebarCategoryOrder(th.Context, user.Id, th.BasicTeam.Id, actualOrder) + require.Nil(t, err, "Should update order successfully") + + // We create a copy of actualOrder to prevent racy read + // of the slice when the broadcast message is sent from webhub. + newOrder := make([]string, len(actualOrder)) + copy(newOrder, actualOrder) + newOrder[2] = "asd" + err = th.App.UpdateSidebarCategoryOrder(th.Context, user.Id, th.BasicTeam.Id, newOrder) + require.NotNil(t, err, "Should return error due to invalid id") + }) + + t.Run("GetSidebarCategoryOrder", func(t *testing.T) { + catOrder, err := th.App.GetSidebarCategoryOrder(th.Context, user.Id, th.BasicTeam.Id) + require.Nil(t, err, "Expected no error") + require.Len(t, catOrder, 5) + require.Equal(t, catOrder[1], createdCategory.Id, "the newly created category should be after favorites") + }) +} + +func TestGetSidebarCategoriesWithAppsCategory(t *testing.T) { + t.Run("should return the sidebar categories for the given user/team", func(t *testing.T) { + th := Setup(t).InitBasicWithAppsSidebarEnabled() + defer th.TearDown() + + _, err := th.App.CreateSidebarCategory(th.Context, th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + DisplayName: "new category", + }, + }) + require.Nil(t, err) + + categories, err := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser.Id, th.BasicTeam.Id) + assert.Nil(t, err) + assert.Len(t, categories.Categories, 5) + }) + + t.Run("should create the initial categories even if migration hasn't ran yet", func(t *testing.T) { + th := Setup(t).InitBasicWithAppsSidebarEnabled() + defer th.TearDown() + + // Manually add the user to the team without going through the app layer to simulate a pre-existing user/team + // relationship that hasn't been migrated yet + team := th.CreateTeam() + _, err := th.App.Srv().Store().Team().SaveMember(&model.TeamMember{ + TeamId: team.Id, + UserId: th.BasicUser.Id, + SchemeUser: true, + }, 100) + require.NoError(t, err) + + categories, appErr := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser.Id, team.Id) + assert.Nil(t, appErr) + assert.Len(t, categories.Categories, 4) + }) + + t.Run("should return a store error if a db table is missing", func(t *testing.T) { + th := Setup(t).InitBasicWithAppsSidebarEnabled() + defer th.TearDown() + + // Temporarily renaming a table to force a DB error. + sqlStore := mainHelper.GetSQLStore() + _, err := sqlStore.GetMasterX().Exec("ALTER TABLE SidebarCategories RENAME TO SidebarCategoriesTest") + require.NoError(t, err) + defer func() { + _, err := sqlStore.GetMasterX().Exec("ALTER TABLE SidebarCategoriesTest RENAME TO SidebarCategories") + require.NoError(t, err) + }() + + categories, appErr := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser.Id, th.BasicTeam.Id) + assert.Nil(t, categories) + assert.NotNil(t, appErr) + assert.Equal(t, "app.channel.sidebar_categories.app_error", appErr.Id) + }) +} + +func TestUpdateSidebarCategoriesWithAppsCategory(t *testing.T) { + t.Run("should mute and unmute all channels in a category when it is muted or unmuted", func(t *testing.T) { + th := Setup(t).InitBasicWithAppsSidebarEnabled() + defer th.TearDown() + + categories, err := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser.Id, th.BasicTeam.Id) + require.Nil(t, err) + + channelsCategory := categories.Categories[1] + + // Create some channels to be part of the channels category + channel1 := th.CreateChannel(th.Context, th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel1) + + channel2 := th.CreateChannel(th.Context, th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel2) + + // Mute the category + updated, err := th.App.UpdateSidebarCategories(th.Context, th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: channelsCategory.Id, + Muted: true, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + }) + require.Nil(t, err) + assert.True(t, updated[0].Muted) + + // Confirm that the channels are now muted + member1, err := th.App.GetChannelMember(th.Context, channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member1.IsChannelMuted()) + member2, err := th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member2.IsChannelMuted()) + + // Unmute the category + updated, err = th.App.UpdateSidebarCategories(th.Context, th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: channelsCategory.Id, + Muted: false, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + }) + require.Nil(t, err) + assert.False(t, updated[0].Muted) + + // Confirm that the channels are now unmuted + member1, err = th.App.GetChannelMember(th.Context, channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member1.IsChannelMuted()) + member2, err = th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member2.IsChannelMuted()) + }) + + t.Run("should mute and unmute channels moved from an unmuted category to a muted one and back", func(t *testing.T) { + th := Setup(t).InitBasicWithAppsSidebarEnabled() + defer th.TearDown() + + // Create some channels + channel1 := th.CreateChannel(th.Context, th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel1) + + channel2 := th.CreateChannel(th.Context, th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel2) + + // And some categories + mutedCategory, err := th.App.CreateSidebarCategory(th.Context, th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "muted", + Muted: true, + }, + }) + require.Nil(t, err) + require.True(t, mutedCategory.Muted) + + unmutedCategory, err := th.App.CreateSidebarCategory(th.Context, th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "unmuted", + Muted: false, + }, + Channels: []string{channel1.Id, channel2.Id}, + }) + require.Nil(t, err) + require.False(t, unmutedCategory.Muted) + + // Move the channels + _, err = th.App.UpdateSidebarCategories(th.Context, th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: mutedCategory.Id, + DisplayName: mutedCategory.DisplayName, + Muted: mutedCategory.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: unmutedCategory.Id, + DisplayName: unmutedCategory.DisplayName, + Muted: unmutedCategory.Muted, + }, + Channels: []string{}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are now muted + member1, err := th.App.GetChannelMember(th.Context, channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member1.IsChannelMuted()) + member2, err := th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member2.IsChannelMuted()) + + // Move the channels back + _, err = th.App.UpdateSidebarCategories(th.Context, th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: mutedCategory.Id, + DisplayName: mutedCategory.DisplayName, + Muted: mutedCategory.Muted, + }, + Channels: []string{}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: unmutedCategory.Id, + DisplayName: unmutedCategory.DisplayName, + Muted: unmutedCategory.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are now unmuted + member1, err = th.App.GetChannelMember(th.Context, channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member1.IsChannelMuted()) + member2, err = th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member2.IsChannelMuted()) + }) + + t.Run("should not mute or unmute channels moved between muted categories", func(t *testing.T) { + th := Setup(t).InitBasicWithAppsSidebarEnabled() + defer th.TearDown() + + // Create some channels + channel1 := th.CreateChannel(th.Context, th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel1) + + channel2 := th.CreateChannel(th.Context, th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel2) + + // And some categories + category1, err := th.App.CreateSidebarCategory(th.Context, th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "category1", + Muted: true, + }, + }) + require.Nil(t, err) + require.True(t, category1.Muted) + + category2, err := th.App.CreateSidebarCategory(th.Context, th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "category2", + Muted: true, + }, + Channels: []string{channel1.Id, channel2.Id}, + }) + require.Nil(t, err) + require.True(t, category2.Muted) + + // Move the unmuted channels + _, err = th.App.UpdateSidebarCategories(th.Context, th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: category1.Id, + DisplayName: category1.DisplayName, + Muted: category1.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: category2.Id, + DisplayName: category2.DisplayName, + Muted: category2.Muted, + }, + Channels: []string{}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are still unmuted + member1, err := th.App.GetChannelMember(th.Context, channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member1.IsChannelMuted()) + member2, err := th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member2.IsChannelMuted()) + + // Mute the channels manually + _, err = th.App.ToggleMuteChannel(th.Context, channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + _, err = th.App.ToggleMuteChannel(th.Context, channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + + // Move the muted channels back + _, err = th.App.UpdateSidebarCategories(th.Context, th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: category1.Id, + DisplayName: category1.DisplayName, + Muted: category1.Muted, + }, + Channels: []string{}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: category2.Id, + DisplayName: category2.DisplayName, + Muted: category2.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are still muted + member1, err = th.App.GetChannelMember(th.Context, channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member1.IsChannelMuted()) + member2, err = th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member2.IsChannelMuted()) + }) + + t.Run("should not mute or unmute channels moved between unmuted categories", func(t *testing.T) { + th := Setup(t).InitBasicWithAppsSidebarEnabled() + defer th.TearDown() + + // Create some channels + channel1 := th.CreateChannel(th.Context, th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel1) + + channel2 := th.CreateChannel(th.Context, th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel2) + + // And some categories + category1, err := th.App.CreateSidebarCategory(th.Context, th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "category1", + Muted: false, + }, + }) + require.Nil(t, err) + require.False(t, category1.Muted) + + category2, err := th.App.CreateSidebarCategory(th.Context, th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "category2", + Muted: false, + }, + Channels: []string{channel1.Id, channel2.Id}, + }) + require.Nil(t, err) + require.False(t, category2.Muted) + + // Move the unmuted channels + _, err = th.App.UpdateSidebarCategories(th.Context, th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: category1.Id, + DisplayName: category1.DisplayName, + Muted: category1.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: category2.Id, + DisplayName: category2.DisplayName, + Muted: category2.Muted, + }, + Channels: []string{}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are still unmuted + member1, err := th.App.GetChannelMember(th.Context, channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member1.IsChannelMuted()) + member2, err := th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member2.IsChannelMuted()) + + // Mute the channels manually + _, err = th.App.ToggleMuteChannel(th.Context, channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + _, err = th.App.ToggleMuteChannel(th.Context, channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + + // Move the muted channels back + _, err = th.App.UpdateSidebarCategories(th.Context, th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: category1.Id, + DisplayName: category1.DisplayName, + Muted: category1.Muted, + }, + Channels: []string{}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: category2.Id, + DisplayName: category2.DisplayName, + Muted: category2.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are still muted + member1, err = th.App.GetChannelMember(th.Context, channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member1.IsChannelMuted()) + member2, err = th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member2.IsChannelMuted()) + }) +} diff --git a/server/channels/app/helper_test.go b/server/channels/app/helper_test.go index 294de97280e..e192560c43c 100644 --- a/server/channels/app/helper_test.go +++ b/server/channels/app/helper_test.go @@ -253,6 +253,12 @@ func (th *TestHelper) InitBasic() *TestHelper { return th } +func (th *TestHelper) InitBasicWithAppsSidebarEnabled() *TestHelper { + th.App.Config().FeatureFlags.AppsSidebarCategory = true + + return th.InitBasic() +} + func (th *TestHelper) DeleteBots() *TestHelper { preexistingBots, _ := th.App.GetBots(&model.BotGetOptions{Page: 0, PerPage: 100}) for _, bot := range preexistingBots { diff --git a/server/channels/app/team.go b/server/channels/app/team.go index c2dd10c6066..66055b4afc9 100644 --- a/server/channels/app/team.go +++ b/server/channels/app/team.go @@ -956,9 +956,11 @@ func (a *App) JoinUserToTeam(c request.CTX, team *model.Team, user *model.User, return nil, model.NewAppError("JoinUserToTeam", "app.user.update_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } + appsCategoryEnabled := a.Config().FeatureFlags.AppsSidebarCategory opts := &store.SidebarCategorySearchOpts{ - TeamID: team.Id, - ExcludeTeam: false, + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: appsCategoryEnabled, } if _, err := a.createInitialSidebarCategories(user.Id, opts); err != nil { mlog.Warn( diff --git a/server/channels/app/team_test.go b/server/channels/app/team_test.go index 14bda61c4ca..c8cda76eafc 100644 --- a/server/channels/app/team_test.go +++ b/server/channels/app/team_test.go @@ -1025,6 +1025,119 @@ func TestJoinUserToTeam(t *testing.T) { }) } +func TestJoinUserToTeamWithAppsCategoryEnabled(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.AppsSidebarCategory = true }) + + id := model.NewId() + team := &model.Team{ + DisplayName: "dn_" + id, + Name: "name" + id, + Email: "success+" + id + "@simulator.amazonses.com", + Type: model.TeamOpen, + } + + _, err := th.App.CreateTeam(th.Context, team) + require.Nil(t, err, "Should create a new team") + + maxUsersPerTeam := th.App.Config().TeamSettings.MaxUsersPerTeam + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.MaxUsersPerTeam = maxUsersPerTeam }) + th.App.PermanentDeleteTeam(th.Context, team) + }() + one := 1 + th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.MaxUsersPerTeam = &one }) + + t.Run("new join", func(t *testing.T) { + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} + ruser, _ := th.App.CreateUser(th.Context, &user) + defer th.App.PermanentDeleteUser(th.Context, &user) + + _, appErr := th.App.JoinUserToTeam(th.Context, team, ruser, "") + require.Nil(t, appErr, "Should return no error") + }) + + t.Run("new join with limit problem", func(t *testing.T) { + user1 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} + ruser1, _ := th.App.CreateUser(th.Context, &user1) + user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} + ruser2, _ := th.App.CreateUser(th.Context, &user2) + + defer th.App.PermanentDeleteUser(th.Context, &user1) + defer th.App.PermanentDeleteUser(th.Context, &user2) + + _, appErr := th.App.JoinUserToTeam(th.Context, team, ruser1, ruser2.Id) + require.Nil(t, appErr, "Should return no error") + + _, appErr = th.App.JoinUserToTeam(th.Context, team, ruser2, ruser1.Id) + require.NotNil(t, appErr, "Should fail") + }) + + t.Run("re-join after leaving with limit problem", func(t *testing.T) { + user1 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} + ruser1, _ := th.App.CreateUser(th.Context, &user1) + + user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} + ruser2, _ := th.App.CreateUser(th.Context, &user2) + + defer th.App.PermanentDeleteUser(th.Context, &user1) + defer th.App.PermanentDeleteUser(th.Context, &user2) + + _, appErr := th.App.JoinUserToTeam(th.Context, team, ruser1, ruser2.Id) + require.Nil(t, appErr, "Should return no error") + appErr = th.App.LeaveTeam(th.Context, team, ruser1, ruser1.Id) + require.Nil(t, appErr, "Should return no error") + _, appErr = th.App.JoinUserToTeam(th.Context, team, ruser2, ruser2.Id) + require.Nil(t, appErr, "Should return no error") + + _, appErr = th.App.JoinUserToTeam(th.Context, team, ruser1, ruser2.Id) + require.NotNil(t, appErr, "Should fail") + }) + + t.Run("new join with correct scheme_admin value from group syncable", func(t *testing.T) { + user1 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} + ruser1, _ := th.App.CreateUser(th.Context, &user1) + defer th.App.PermanentDeleteUser(th.Context, &user1) + + group := th.CreateGroup() + + _, err = th.App.UpsertGroupMember(group.Id, user1.Id) + require.Nil(t, err) + + gs, err := th.App.UpsertGroupSyncable(&model.GroupSyncable{ + AutoAdd: true, + SyncableId: team.Id, + Type: model.GroupSyncableTypeTeam, + GroupId: group.Id, + SchemeAdmin: false, + }) + require.Nil(t, err) + + th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.MaxUsersPerTeam = model.NewInt(999) }) + + tm1, appErr := th.App.JoinUserToTeam(th.Context, team, ruser1, "") + require.Nil(t, appErr) + require.False(t, tm1.SchemeAdmin) + + user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} + ruser2, _ := th.App.CreateUser(th.Context, &user2) + defer th.App.PermanentDeleteUser(th.Context, &user2) + + _, err = th.App.UpsertGroupMember(group.Id, user2.Id) + require.Nil(t, err) + + gs.SchemeAdmin = true + _, err = th.App.UpdateGroupSyncable(gs) + require.Nil(t, err) + + tm2, appErr := th.App.JoinUserToTeam(th.Context, team, ruser2, "") + require.Nil(t, appErr) + require.True(t, tm2.SchemeAdmin) + }) +} + func TestLeaveTeamPanic(t *testing.T) { th := SetupWithStoreMock(t) defer th.TearDown() diff --git a/server/channels/store/layer_generators/main.go b/server/channels/store/layer_generators/main.go index cfb95abdb82..a1dac2b6677 100644 --- a/server/channels/store/layer_generators/main.go +++ b/server/channels/store/layer_generators/main.go @@ -296,6 +296,8 @@ func generateLayer(name, templateFile string) ([]byte, error) { paramsWithType = append(paramsWithType, fmt.Sprintf("%s store.%s", param.Name, param.Type)) case "*UserGetByIdsOpts", "*ChannelMemberGraphQLSearchOpts", "*SidebarCategorySearchOpts": paramsWithType = append(paramsWithType, fmt.Sprintf("%s *store.%s", param.Name, strings.TrimPrefix(param.Type, "*"))) + case "...*SidebarCategorySearchOpts": + paramsWithType = append(paramsWithType, fmt.Sprintf("%s ...*store.%s", param.Name, strings.TrimPrefix(param.Type, "...*"))) default: paramsWithType = append(paramsWithType, fmt.Sprintf("%s %s", param.Name, param.Type)) } @@ -310,6 +312,8 @@ func generateLayer(name, templateFile string) ([]byte, error) { paramsWithType = append(paramsWithType, fmt.Sprintf("%s store.%s", param.Name, param.Type)) case "*UserGetByIdsOpts", "*ChannelMemberGraphQLSearchOpts", "*SidebarCategorySearchOpts": paramsWithType = append(paramsWithType, fmt.Sprintf("%s *store.%s", param.Name, strings.TrimPrefix(param.Type, "*"))) + case "...*SidebarCategorySearchOpts": + paramsWithType = append(paramsWithType, fmt.Sprintf("%s ...*store.%s", param.Name, strings.TrimPrefix(param.Type, "...*"))) default: paramsWithType = append(paramsWithType, fmt.Sprintf("%s %s", param.Name, param.Type)) } diff --git a/server/channels/store/opentracinglayer/opentracinglayer.go b/server/channels/store/opentracinglayer/opentracinglayer.go index 66a50512607..f70d14eddc0 100644 --- a/server/channels/store/opentracinglayer/opentracinglayer.go +++ b/server/channels/store/opentracinglayer/opentracinglayer.go @@ -809,7 +809,7 @@ func (s *OpenTracingLayerChannelStore) CreateInitialSidebarCategories(userID str return result, err } -func (s *OpenTracingLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) { +func (s *OpenTracingLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels, options ...*store.SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CreateSidebarCategory") s.Root.Store.SetContext(newCtx) @@ -818,7 +818,7 @@ func (s *OpenTracingLayerChannelStore) CreateSidebarCategory(userID string, team }() defer span.Finish() - result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory) + result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory, options...) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -1043,6 +1043,24 @@ func (s *OpenTracingLayerChannelStore) GetAllDirectChannelsForExportAfter(limit return result, err } +func (s *OpenTracingLayerChannelStore) GetBotChannelsByUser(userID string, opts store.ChannelSearchOpts) (model.ChannelList, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetBotChannelsByUser") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ChannelStore.GetBotChannelsByUser(userID, opts) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetByName") @@ -1740,7 +1758,7 @@ func (s *OpenTracingLayerChannelStore) GetSidebarCategories(userID string, opts return result, err } -func (s *OpenTracingLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) { +func (s *OpenTracingLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string, options ...*store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetSidebarCategoriesForTeamForUser") s.Root.Store.SetContext(newCtx) @@ -1749,7 +1767,7 @@ func (s *OpenTracingLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID }() defer span.Finish() - result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID) + result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID, options...) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -1758,7 +1776,7 @@ func (s *OpenTracingLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID return result, err } -func (s *OpenTracingLayerChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) { +func (s *OpenTracingLayerChannelStore) GetSidebarCategory(categoryID string, options ...*store.SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetSidebarCategory") s.Root.Store.SetContext(newCtx) @@ -1767,7 +1785,7 @@ func (s *OpenTracingLayerChannelStore) GetSidebarCategory(categoryID string) (*m }() defer span.Finish() - result, err := s.ChannelStore.GetSidebarCategory(categoryID) + result, err := s.ChannelStore.GetSidebarCategory(categoryID, options...) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -2600,7 +2618,7 @@ func (s *OpenTracingLayerChannelStore) UpdateMultipleMembers(members []*model.Ch return result, err } -func (s *OpenTracingLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { +func (s *OpenTracingLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels, options ...*store.SidebarCategorySearchOpts) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateSidebarCategories") s.Root.Store.SetContext(newCtx) @@ -2609,7 +2627,7 @@ func (s *OpenTracingLayerChannelStore) UpdateSidebarCategories(userID string, te }() defer span.Finish() - result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories) + result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories, options...) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) diff --git a/server/channels/store/retrylayer/retrylayer.go b/server/channels/store/retrylayer/retrylayer.go index b39c79ab9bf..37d71bd2d6d 100644 --- a/server/channels/store/retrylayer/retrylayer.go +++ b/server/channels/store/retrylayer/retrylayer.go @@ -871,11 +871,11 @@ func (s *RetryLayerChannelStore) CreateInitialSidebarCategories(userID string, o } -func (s *RetryLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) { +func (s *RetryLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels, options ...*store.SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error) { tries := 0 for { - result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory) + result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory, options...) if err == nil { return result, nil } @@ -1144,6 +1144,27 @@ func (s *RetryLayerChannelStore) GetAllDirectChannelsForExportAfter(limit int, a } +func (s *RetryLayerChannelStore) GetBotChannelsByUser(userID string, opts store.ChannelSearchOpts) (model.ChannelList, error) { + + tries := 0 + for { + result, err := s.ChannelStore.GetBotChannelsByUser(userID, opts) + 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 *RetryLayerChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) { tries := 0 @@ -1948,11 +1969,11 @@ func (s *RetryLayerChannelStore) GetSidebarCategories(userID string, opts *store } -func (s *RetryLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) { +func (s *RetryLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string, options ...*store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) { tries := 0 for { - result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID) + result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID, options...) if err == nil { return result, nil } @@ -1969,11 +1990,11 @@ func (s *RetryLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID strin } -func (s *RetryLayerChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) { +func (s *RetryLayerChannelStore) GetSidebarCategory(categoryID string, options ...*store.SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error) { tries := 0 for { - result, err := s.ChannelStore.GetSidebarCategory(categoryID) + result, err := s.ChannelStore.GetSidebarCategory(categoryID, options...) if err == nil { return result, nil } @@ -2878,11 +2899,11 @@ func (s *RetryLayerChannelStore) UpdateMultipleMembers(members []*model.ChannelM } -func (s *RetryLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { +func (s *RetryLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels, options ...*store.SidebarCategorySearchOpts) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { tries := 0 for { - result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories) + result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories, options...) if err == nil { return result, resultVar1, nil } diff --git a/server/channels/store/sqlstore/channel_store.go b/server/channels/store/sqlstore/channel_store.go index 38c35239c03..73787b6d987 100644 --- a/server/channels/store/sqlstore/channel_store.go +++ b/server/channels/store/sqlstore/channel_store.go @@ -1190,6 +1190,38 @@ func (s SqlChannelStore) GetChannelsByUser(userId string, includeDeleted bool, l return channels, nil } +func (s SqlChannelStore) GetBotChannelsByUser(userId string, opts store.ChannelSearchOpts) (model.ChannelList, error) { + query := s.getQueryBuilder(). + Select("C.*"). + From("Channels as C, ChannelMembers as CM1, ChannelMembers as CM2, Bots as B"). + Where(sq.And{ + sq.Expr("C.Id = CM1.ChannelId"), + sq.Eq{"CM1.UserId": userId}, + sq.Eq{"C.Type": model.ChannelTypeDirect}, + sq.Expr("C.Id = CM2.ChannelId"), + sq.Expr("CM2.UserId = B.UserId"), + sq.Eq{"B.DeleteAt": 0}, + }). + OrderBy("C.Id ASC") + + if !opts.IncludeDeleted { + query = query.Where(sq.Eq{"C.DeleteAt": int(0)}) + } + + sql, args, err := query.ToSql() + if err != nil { + return nil, errors.Wrap(err, "GetBotChannelsByUser_ToSql") + } + + channels := model.ChannelList{} + err = s.GetReplicaX().Select(&channels, sql, args...) + if err != nil { + return nil, errors.Wrapf(err, "failed to get bot channels with UserId=%s", userId) + } + + return channels, nil +} + func (s SqlChannelStore) GetAllChannelMembersById(channelID string) ([]string, error) { sql, args, err := s.channelMembersForTeamWithSchemeSelectQuery.Where(sq.Eq{ "ChannelId": channelID, diff --git a/server/channels/store/sqlstore/channel_store_categories.go b/server/channels/store/sqlstore/channel_store_categories.go index 4eca0d5de07..1481ae0b1b4 100644 --- a/server/channels/store/sqlstore/channel_store_categories.go +++ b/server/channels/store/sqlstore/channel_store_categories.go @@ -58,11 +58,7 @@ func (s SqlChannelStore) createInitialSidebarCategoriesT(transaction *sqlxTxWrap From("SidebarCategories"). Where(sq.Eq{ "UserId": userId, - "Type": []model.SidebarCategoryType{ - model.SidebarCategoryFavorites, - model.SidebarCategoryChannels, - model.SidebarCategoryDirectMessages, - }, + "Type": model.SystemSidebarCategories, }) if !opts.ExcludeTeam { @@ -157,6 +153,15 @@ func (s SqlChannelStore) createInitialSidebarCategoriesT(transaction *sqlxTxWrap hasInsert = true } + if opts.AppsCategoryEnabled { + teamIDs = getRequiredTeamIDs(model.SidebarCategoryApps, opts) + for _, teamID := range teamIDs { + appsCategoryId := fmt.Sprintf("%s_%s_%s", model.SidebarCategoryApps, userId, teamID) + insertBuilder = insertBuilder.Values(appsCategoryId, userId, teamID, model.DefaultSidebarSortOrderApps, model.SidebarCategorySortDefault, model.SidebarCategoryApps, "Apps" /* This will be retranslated by the client into the user's locale */, false, false) + hasInsert = true + } + } + if hasInsert { sql, args, err := insertBuilder.ToSql() if err != nil { @@ -296,7 +301,7 @@ type sidebarCategoryForJoin struct { ChannelId *string } -func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategory *model.SidebarCategoryWithChannels) (_ *model.SidebarCategoryWithChannels, err error) { +func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategory *model.SidebarCategoryWithChannels, options ...*store.SidebarCategorySearchOpts) (_ *model.SidebarCategoryWithChannels, err error) { transaction, err := s.GetMasterX().Beginx() if err != nil { return nil, errors.Wrap(err, "begin_transaction") @@ -304,9 +309,15 @@ func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategor defer finalizeTransactionX(transaction, &err) + appsCategoryEnabled := false + if len(options) > 0 { + appsCategoryEnabled = options[0].AppsCategoryEnabled + } + opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, - ExcludeTeam: false, + TeamID: teamId, + ExcludeTeam: false, + AppsCategoryEnabled: appsCategoryEnabled, } categoriesWithOrder, err := s.getSidebarCategoriesT(transaction, userId, opts) if err != nil { @@ -418,14 +429,14 @@ func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategor return result, nil } -func (s SqlChannelStore) completePopulatingCategoryChannels(category *model.SidebarCategoryWithChannels) (_ *model.SidebarCategoryWithChannels, err error) { +func (s SqlChannelStore) completePopulatingCategoryChannels(category *model.SidebarCategoryWithChannels, appsCategoryEnabled bool) (_ *model.SidebarCategoryWithChannels, err error) { transaction, err := s.GetMasterX().Beginx() if err != nil { return nil, errors.Wrap(err, "begin_transaction") } defer finalizeTransactionX(transaction, &err) - result, err := s.completePopulatingCategoryChannelsT(transaction, category) + result, err := s.completePopulatingCategoryChannelsT(transaction, category, appsCategoryEnabled) if err != nil { return nil, err } @@ -437,15 +448,38 @@ func (s SqlChannelStore) completePopulatingCategoryChannels(category *model.Side return result, nil } -func (s SqlChannelStore) completePopulatingCategoryChannelsT(db dbSelecter, category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) { +func (s SqlChannelStore) completePopulatingCategoryChannelsT(db dbSelecter, category *model.SidebarCategoryWithChannels, appsCategoryEnabled bool) (*model.SidebarCategoryWithChannels, error) { if category.Type == model.SidebarCategoryCustom || category.Type == model.SidebarCategoryFavorites { return category, nil } var channelTypeFilter sq.Sqlizer - if category.Type == model.SidebarCategoryDirectMessages { - // any DM/GM channels that aren't in any category should be returned as part of the Direct Messages category - channelTypeFilter = sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}} + if category.Type == model.SidebarCategoryApps || category.Type == model.SidebarCategoryDirectMessages { + botChannelsIDs := []string{} + if appsCategoryEnabled { + botChannels, err := s.SqlStore.stores.channel.GetBotChannelsByUser(category.UserId, store.ChannelSearchOpts{}) + if err != nil { + return nil, err + } + + for _, botChannel := range botChannels { + botChannelsIDs = append(botChannelsIDs, botChannel.Id) + } + } + + if category.Type == model.SidebarCategoryApps { + // any DM channels with Bots that aren't in any category should be returned as part of the Apps category + channelTypeFilter = sq.And{ + sq.Eq{"Channels.Type": model.ChannelTypeDirect}, + sq.Eq{"Channels.Id": botChannelsIDs}, + } + } else if category.Type == model.SidebarCategoryDirectMessages { + // any DM/GM channels with Users that aren't in any category should be returned as part of the Direct Messages category + channelTypeFilter = sq.And{ + sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}}, + sq.NotEq{"Channels.Id": botChannelsIDs}, + } + } } else if category.Type == model.SidebarCategoryChannels { // any public/private channels that are on the current team and aren't in any category should be returned as part of the Channels category channelTypeFilter = sq.And{ @@ -468,6 +502,12 @@ func (s SqlChannelStore) completePopulatingCategoryChannelsT(db dbSelecter, cate }). Suffix(")") + if !appsCategoryEnabled { + doesNotHaveSidebarChannel = doesNotHaveSidebarChannel.Where( + sq.NotEq{"SidebarCategories.Type": model.SidebarCategoryApps}, + ) + } + channels := []string{} sql, args, err := s.getQueryBuilder(). Select("Id"). @@ -492,7 +532,7 @@ func (s SqlChannelStore) completePopulatingCategoryChannelsT(db dbSelecter, cate return category, nil } -func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, error) { +func (s SqlChannelStore) GetSidebarCategory(categoryId string, options ...*store.SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error) { sql, args, err := s.getQueryBuilder(). Select("SidebarCategories.*", "SidebarChannels.ChannelId"). From("SidebarCategories"). @@ -521,7 +561,13 @@ func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCa result.Channels = append(result.Channels, *category.ChannelId) } } - return s.completePopulatingCategoryChannels(result) + + appsCategoryEnabled := false + if len(options) > 0 { + appsCategoryEnabled = options[0].AppsCategoryEnabled + } + + return s.completePopulatingCategoryChannels(result, appsCategoryEnabled) } func (s SqlChannelStore) getSidebarCategoriesT(db dbSelecter, userId string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) { @@ -553,6 +599,10 @@ func (s SqlChannelStore) getSidebarCategoriesT(db dbSelecter, userId string, opt query = query.Where(sq.Eq{"SidebarCategories.TeamId": opts.TeamID}) } + if !opts.AppsCategoryEnabled { + query = query.Where(sq.NotEq{"SidebarCategories.Type": model.SidebarCategoryApps}) + } + sql, args, err := query.ToSql() if err != nil { return nil, errors.Wrap(err, "sidebar_categories_tosql") @@ -583,7 +633,7 @@ func (s SqlChannelStore) getSidebarCategoriesT(db dbSelecter, userId string, opt } } for _, category := range oc.Categories { - if _, err := s.completePopulatingCategoryChannelsT(db, category); err != nil { + if _, err := s.completePopulatingCategoryChannelsT(db, category, opts.AppsCategoryEnabled); err != nil { return nil, err } } @@ -591,10 +641,16 @@ func (s SqlChannelStore) getSidebarCategoriesT(db dbSelecter, userId string, opt return &oc, nil } -func (s SqlChannelStore) GetSidebarCategoriesForTeamForUser(userId, teamId string) (*model.OrderedSidebarCategories, error) { +func (s SqlChannelStore) GetSidebarCategoriesForTeamForUser(userId, teamId string, options ...*store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) { + appsCategoryEnabled := false + if len(options) > 0 { + appsCategoryEnabled = options[0].AppsCategoryEnabled + } + opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, - ExcludeTeam: false, + TeamID: teamId, + ExcludeTeam: false, + AppsCategoryEnabled: appsCategoryEnabled, } return s.getSidebarCategoriesT(s.GetReplicaX(), userId, opts) } @@ -688,17 +744,22 @@ func (s SqlChannelStore) UpdateSidebarCategoryOrder(userId, teamId string, categ } //nolint:unparam -func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) (updated []*model.SidebarCategoryWithChannels, original []*model.SidebarCategoryWithChannels, err error) { +func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels, options ...*store.SidebarCategorySearchOpts) (updated []*model.SidebarCategoryWithChannels, original []*model.SidebarCategoryWithChannels, err error) { transaction, err := s.GetMasterX().Beginx() if err != nil { return nil, nil, errors.Wrap(err, "begin_transaction") } defer finalizeTransactionX(transaction, &err) + opts := &store.SidebarCategorySearchOpts{} + if len(options) > 0 { + opts = options[0] + } + updatedCategories := []*model.SidebarCategoryWithChannels{} originalCategories := []*model.SidebarCategoryWithChannels{} for _, category := range categories { - srcCategory, err2 := s.GetSidebarCategory(category.Id) + srcCategory, err2 := s.GetSidebarCategory(category.Id, opts) if err2 != nil { return nil, nil, errors.Wrap(err2, "failed to find SidebarCategories") } @@ -719,7 +780,7 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori destCategory.DisplayName = srcCategory.DisplayName } - if destCategory.Type != model.SidebarCategoryDirectMessages { + if destCategory.Type != model.SidebarCategoryDirectMessages && destCategory.Type != model.SidebarCategoryApps { destCategory.Channels = make([]string, len(category.Channels)) copy(destCategory.Channels, category.Channels) @@ -746,7 +807,7 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori } // if we are updating DM category, it's order can't channel order cannot be changed. - if category.Type != model.SidebarCategoryDirectMessages { + if category.Type != model.SidebarCategoryDirectMessages && destCategory.Type != model.SidebarCategoryApps { // Remove any SidebarChannels entries that were either: // - previously in this category (and any ones that are still in the category will be recreated below) // - in another category and are being added to this category @@ -843,7 +904,7 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori // Ensure Channels are populated for Channels/Direct Messages category if they change for i, updatedCategory := range updatedCategories { - populated, nErr := s.completePopulatingCategoryChannelsT(transaction, updatedCategory) + populated, nErr := s.completePopulatingCategoryChannelsT(transaction, updatedCategory, opts.AppsCategoryEnabled) if nErr != nil { return nil, nil, nErr } diff --git a/server/channels/store/sqlstore/channel_store_categories_test.go b/server/channels/store/sqlstore/channel_store_categories_test.go index 5de3b55953d..1514b1ac98a 100644 --- a/server/channels/store/sqlstore/channel_store_categories_test.go +++ b/server/channels/store/sqlstore/channel_store_categories_test.go @@ -11,4 +11,5 @@ import ( func TestChannelStoreCategories(t *testing.T) { StoreTestWithSqlStore(t, storetest.TestChannelStoreCategories) + StoreTestWithSqlStore(t, storetest.TestChannelStoreCategoriesWithAppsCategory) } diff --git a/server/channels/store/store.go b/server/channels/store/store.go index cd813239d4d..f9738bcf441 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -197,6 +197,7 @@ type ChannelStore interface { GetChannels(teamID, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, error) + GetBotChannelsByUser(userID string, opts ChannelSearchOpts) (model.ChannelList, error) GetAllChannelMembersById(id string) ([]string, error) GetAllChannels(page, perPage int, opts ChannelSearchOpts) (model.ChannelListWithTeamData, error) GetAllChannelsCount(opts ChannelSearchOpts) (int64, error) @@ -271,13 +272,13 @@ type ChannelStore interface { ResetAllChannelSchemes() error ClearAllCustomRoleAssignments() error CreateInitialSidebarCategories(userID string, opts *SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) - GetSidebarCategoriesForTeamForUser(userID, teamID string) (*model.OrderedSidebarCategories, error) + GetSidebarCategoriesForTeamForUser(userID, teamID string, options ...*SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) GetSidebarCategories(userID string, opts *SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) - GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) + GetSidebarCategory(categoryID string, options ...*SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error) GetSidebarCategoryOrder(userID, teamID string) ([]string, error) - CreateSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) + CreateSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels, options ...*SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error) UpdateSidebarCategoryOrder(userID, teamID string, categoryOrder []string) error - UpdateSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) + UpdateSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels, options ...*SidebarCategorySearchOpts) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) UpdateSidebarChannelsByPreferences(preferences model.Preferences) error DeleteSidebarChannelsByPreferences(preferences model.Preferences) error DeleteSidebarCategory(categoryID string) error @@ -1091,8 +1092,9 @@ type PostReminderMetadata struct { // SidebarCategorySearchOpts contains the options for a graphQL query // to get the sidebar categories. type SidebarCategorySearchOpts struct { - TeamID string - ExcludeTeam bool + TeamID string + ExcludeTeam bool + AppsCategoryEnabled bool } // Ensure store service adapter implements `product.StoreService` diff --git a/server/channels/store/storetest/channel_store_categories_with_apps_category.go b/server/channels/store/storetest/channel_store_categories_with_apps_category.go new file mode 100644 index 00000000000..550085f8f7d --- /dev/null +++ b/server/channels/store/storetest/channel_store_categories_with_apps_category.go @@ -0,0 +1,2681 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package storetest + +import ( + "database/sql" + "errors" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/server/v8/channels/store" + "github.com/mattermost/mattermost-server/server/v8/model" +) + +func TestChannelStoreCategoriesWithAppsCategory(t *testing.T, ss store.Store, s SqlStore) { + t.Run("CreateInitialSidebarCategoriesWithAppsCategory", func(t *testing.T) { testCreateInitialSidebarCategoriesWithAppsCategory(t, ss) }) + t.Run("CreateSidebarCategoryWithAppsCategory", func(t *testing.T) { testCreateSidebarCategoryWithAppsCategory(t, ss) }) + t.Run("GetSidebarCategoryWithAppsCategory", func(t *testing.T) { testGetSidebarCategoryWithAppsCategory(t, ss, s) }) + t.Run("GetSidebarCategoriesWithAppsCategory", func(t *testing.T) { testGetSidebarCategoriesWithAppsCategory(t, ss) }) + t.Run("UpdateSidebarCategoriesWithAppsCategory", func(t *testing.T) { testUpdateSidebarCategoriesWithAppsCategory(t, ss) }) + t.Run("ClearSidebarOnTeamLeaveWithAppsCategory", func(t *testing.T) { testClearSidebarOnTeamLeaveWithAppsCategory(t, ss, s) }) + t.Run("DeleteSidebarCategoryWithAppsCategory", func(t *testing.T) { testDeleteSidebarCategoryWithAppsCategory(t, ss, s) }) + t.Run("UpdateSidebarChannelsByPreferencesWithAppsCategory", func(t *testing.T) { testUpdateSidebarChannelsByPreferencesWithAppsCategory(t, ss) }) + t.Run("SidebarCategoryDeadlockWithAppsCategory", func(t *testing.T) { testSidebarCategoryDeadlockWithAppsCategory(t, ss) }) +} + +func testCreateInitialSidebarCategoriesWithAppsCategory(t *testing.T, ss store.Store) { + t.Run("should create initial favorites/channels/DMs/apps categories", func(t *testing.T) { + userId := model.NewId() + + team := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + assert.NoError(t, nErr) + require.Len(t, res.Categories, 4) + assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) + assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) + assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) + assert.Equal(t, model.SidebarCategoryApps, res.Categories[3].Type) + + res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + assert.NoError(t, err) + assert.Equal(t, res, res2) + }) + + t.Run("should create initial favorites/channels/DMs/apps categories for multiple users", func(t *testing.T) { + userId := model.NewId() + userId2 := model.NewId() + + team := setupTeam(t, ss, userId, userId2) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + res, nErr = ss.Channel().CreateInitialSidebarCategories(userId2, opts) + assert.NoError(t, nErr) + assert.Len(t, res.Categories, 4) + assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) + assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) + assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) + assert.Equal(t, model.SidebarCategoryApps, res.Categories[3].Type) + + res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId2, team.Id, opts) + assert.NoError(t, err) + assert.Equal(t, res, res2) + }) + + t.Run("should create initial favorites/channels/DMs/apps categories on different teams", func(t *testing.T) { + userId := model.NewId() + + team := setupTeam(t, ss, userId) + team2 := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + opts = &store.SidebarCategorySearchOpts{ + TeamID: team2.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts) + assert.NoError(t, nErr) + assert.Len(t, res.Categories, 4) + assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) + assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) + assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) + assert.Equal(t, model.SidebarCategoryApps, res.Categories[3].Type) + + res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id, opts) + assert.NoError(t, err) + assert.Equal(t, res, res2) + }) + + t.Run("shouldn't create additional categories when ones already exist", func(t *testing.T) { + userId := model.NewId() + + team := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Equal(t, res, initialCategories) + + // Calling CreateInitialSidebarCategories a second time shouldn't create any new categories + res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts) + assert.NoError(t, nErr) + assert.NotEmpty(t, res) + + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + assert.NoError(t, err) + assert.Equal(t, initialCategories.Categories, res.Categories) + }) + + t.Run("shouldn't create additional categories when ones already exist even when ran simultaneously", func(t *testing.T) { + userId := model.NewId() + + team := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + + var wg sync.WaitGroup + + for i := 0; i < 10; i++ { + wg.Add(1) + + go func() { + defer wg.Done() + + _, _ = ss.Channel().CreateInitialSidebarCategories(userId, opts) + }() + } + + wg.Wait() + + res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + assert.NoError(t, err) + assert.Len(t, res.Categories, 4) + }) + + t.Run("should populate the Favorites category with regular channels", func(t *testing.T) { + userId := model.NewId() + + team := setupTeam(t, ss, userId) + + // Set up two channels, one favorited and one not + channel1, nErr := ss.Channel().Save(&model.Channel{ + TeamId: team.Id, + Type: model.ChannelTypeOpen, + Name: "channel1", + }, 1000) + require.NoError(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel1.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, err) + + channel2, nErr := ss.Channel().Save(&model.Channel{ + TeamId: team.Id, + Type: model.ChannelTypeOpen, + Name: "channel2", + }, 1000) + require.NoError(t, nErr) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel2.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, err) + + nErr = ss.Preference().Save(model.Preferences{ + { + UserId: userId, + Category: model.PreferenceCategoryFavoriteChannel, + Name: channel1.Id, + Value: "true", + }, + }) + require.NoError(t, nErr) + + // Create the categories + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.Len(t, categories.Categories, 4) + assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) + assert.Equal(t, []string{channel1.Id}, categories.Categories[0].Channels) + assert.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type) + assert.Equal(t, []string{channel2.Id}, categories.Categories[1].Channels) + + // Get and check the categories for channels + categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, nErr) + require.Equal(t, categories, categories2) + }) + + t.Run("should populate the Favorites category in alphabetical order", func(t *testing.T) { + userId := model.NewId() + + team := setupTeam(t, ss, userId) + + // Set up two channels + channel1, nErr := ss.Channel().Save(&model.Channel{ + TeamId: team.Id, + Type: model.ChannelTypeOpen, + Name: "channel1", + DisplayName: "zebra", + }, 1000) + require.NoError(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel1.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, err) + + channel2, nErr := ss.Channel().Save(&model.Channel{ + TeamId: team.Id, + Type: model.ChannelTypeOpen, + Name: "channel2", + DisplayName: "aardvark", + }, 1000) + require.NoError(t, nErr) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel2.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, err) + + nErr = ss.Preference().Save(model.Preferences{ + { + UserId: userId, + Category: model.PreferenceCategoryFavoriteChannel, + Name: channel1.Id, + Value: "true", + }, + { + UserId: userId, + Category: model.PreferenceCategoryFavoriteChannel, + Name: channel2.Id, + Value: "true", + }, + }) + require.NoError(t, nErr) + + // Create the categories + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.Len(t, categories.Categories, 4) + assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) + assert.Equal(t, []string{channel2.Id, channel1.Id}, categories.Categories[0].Channels) + + // Get and check the categories for channels + categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, nErr) + require.Equal(t, categories, categories2) + }) + + t.Run("should populate the Favorites category with DMs and GMs", func(t *testing.T) { + userId := model.NewId() + + team := setupTeam(t, ss, userId) + + otherUserId1 := model.NewId() + otherUserId2 := model.NewId() + + // Set up two direct channels, one favorited and one not + dmChannel1, err := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId1), + Type: model.ChannelTypeDirect, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId1, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.NoError(t, err) + + dmChannel2, err := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId2), + Type: model.ChannelTypeDirect, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId2, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.NoError(t, err) + + err = ss.Preference().Save(model.Preferences{ + { + UserId: userId, + Category: model.PreferenceCategoryFavoriteChannel, + Name: dmChannel1.Id, + Value: "true", + }, + }) + require.NoError(t, err) + + // Create the categories + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.Len(t, categories.Categories, 4) + assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) + assert.Equal(t, []string{dmChannel1.Id}, categories.Categories[0].Channels) + assert.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) + assert.Equal(t, []string{dmChannel2.Id}, categories.Categories[2].Channels) + + // Get and check the categories for channels + categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Equal(t, categories, categories2) + }) + + t.Run("should not populate the Favorites category with channels from other teams", func(t *testing.T) { + userId := model.NewId() + + team := setupTeam(t, ss, userId) + team2 := setupTeam(t, ss, userId) + + // Set up a channel on another team and favorite it + channel1, nErr := ss.Channel().Save(&model.Channel{ + TeamId: team2.Id, + Type: model.ChannelTypeOpen, + Name: "channel1", + }, 1000) + require.NoError(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel1.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, err) + + nErr = ss.Preference().Save(model.Preferences{ + { + UserId: userId, + Category: model.PreferenceCategoryFavoriteChannel, + Name: channel1.Id, + Value: "true", + }, + }) + require.NoError(t, nErr) + + // Create the categories + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.Len(t, categories.Categories, 4) + assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) + assert.Equal(t, []string{}, categories.Categories[0].Channels) + assert.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type) + assert.Equal(t, []string{}, categories.Categories[1].Channels) + + // Get and check the categories for channels + categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, nErr) + require.Equal(t, categories, categories2) + }) + + t.Run("graphQL path to create initial favorites/channels/DMs categories on different teams", func(t *testing.T) { + userId := model.NewId() + + t1 := &model.Team{ + DisplayName: "DisplayName", + Name: NewTestId(), + Email: MakeEmail(), + Type: model.TeamOpen, + InviteId: model.NewId(), + } + t1, err := ss.Team().Save(t1) + require.NoError(t, err) + + m1 := &model.TeamMember{TeamId: t1.Id, UserId: userId} + _, nErr := ss.Team().SaveMember(m1, -1) + require.NoError(t, nErr) + + t2 := &model.Team{ + DisplayName: "DisplayName2", + Name: NewTestId(), + Email: MakeEmail(), + Type: model.TeamOpen, + InviteId: model.NewId(), + } + t2, err = ss.Team().Save(t2) + require.NoError(t, err) + + m2 := &model.TeamMember{TeamId: t2.Id, UserId: userId} + _, nErr = ss.Team().SaveMember(m2, -1) + require.NoError(t, nErr) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: t1.Id, + ExcludeTeam: true, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + for _, cat := range res.Categories { + assert.Equal(t, t2.Id, cat.TeamId) + } + }) +} + +func testCreateSidebarCategoryWithAppsCategory(t *testing.T, ss store.Store) { + t.Run("Creating category without initial categories should fail", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + opts := &store.SidebarCategorySearchOpts{ + AppsCategoryEnabled: true, + } + + // Create the category + created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: model.NewId(), + }, + }, + opts, + ) + require.Error(t, err) + var errNotFound *store.ErrNotFound + require.ErrorAs(t, err, &errNotFound) + require.Nil(t, created) + }) + + t.Run("should place the new category second if Favorites comes first", func(t *testing.T) { + userId := model.NewId() + + team := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + // Create the category + created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: model.NewId(), + }, + }, + opts, + ) + require.NoError(t, err) + + // Confirm that it comes second + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Len(t, res.Categories, 5) + assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) + assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type) + assert.Equal(t, created.Id, res.Categories[1].Id) + }) + + t.Run("should place the new category first if Favorites is not first", func(t *testing.T) { + userId := model.NewId() + + team := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + // Re-arrange the categories so that Favorites comes last + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Len(t, categories.Categories, 4) + require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) + + err = ss.Channel().UpdateSidebarCategoryOrder(userId, team.Id, []string{ + categories.Categories[1].Id, + categories.Categories[2].Id, + categories.Categories[3].Id, + categories.Categories[0].Id, + }) + require.NoError(t, err) + + // Create the category + created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: model.NewId(), + }, + }, + opts, + ) + require.NoError(t, err) + + // Confirm that it comes first + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Len(t, res.Categories, 5) + assert.Equal(t, model.SidebarCategoryCustom, res.Categories[0].Type) + assert.Equal(t, created.Id, res.Categories[0].Id) + }) + + t.Run("should create the category with its channels", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + // Create some channels + channel1, err := ss.Channel().Save(&model.Channel{ + Type: model.ChannelTypeOpen, + TeamId: team.Id, + Name: model.NewId(), + }, 100) + require.NoError(t, err) + channel2, err := ss.Channel().Save(&model.Channel{ + Type: model.ChannelTypeOpen, + TeamId: team.Id, + Name: model.NewId(), + }, 100) + require.NoError(t, err) + + // Create the category + created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: model.NewId(), + }, + Channels: []string{channel2.Id, channel1.Id}, + }, + opts, + ) + require.NoError(t, err) + assert.Equal(t, []string{channel2.Id, channel1.Id}, created.Channels) + + // Get the channel again to ensure that the SidebarChannels were saved correctly + res2, err := ss.Channel().GetSidebarCategory(created.Id, opts) + require.NoError(t, err) + assert.Equal(t, []string{channel2.Id, channel1.Id}, res2.Channels) + }) + + t.Run("should remove any channels from their previous categories", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Len(t, categories.Categories, 4) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + + // Create some channels + channel1, nErr := ss.Channel().Save(&model.Channel{ + Type: model.ChannelTypeOpen, + TeamId: team.Id, + Name: model.NewId(), + }, 100) + require.NoError(t, nErr) + channel2, nErr := ss.Channel().Save(&model.Channel{ + Type: model.ChannelTypeOpen, + TeamId: team.Id, + Name: model.NewId(), + }, 100) + require.NoError(t, nErr) + + // Assign them to categories + favoritesCategory.Channels = []string{channel1.Id} + channelsCategory.Channels = []string{channel2.Id} + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + favoritesCategory, + channelsCategory, + }, + opts, + ) + require.NoError(t, err) + + // Create the category + created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: model.NewId(), + }, + Channels: []string{channel2.Id, channel1.Id}, + }, + opts, + ) + require.NoError(t, err) + assert.Equal(t, []string{channel2.Id, channel1.Id}, created.Channels) + + // Confirm that the channels were removed from their original categories + res2, err := ss.Channel().GetSidebarCategory(favoritesCategory.Id, opts) + require.NoError(t, err) + assert.Equal(t, []string{}, res2.Channels) + + res2, err = ss.Channel().GetSidebarCategory(channelsCategory.Id, opts) + require.NoError(t, err) + assert.Equal(t, []string{}, res2.Channels) + }) +} + +func testGetSidebarCategoryWithAppsCategory(t *testing.T, ss store.Store, s SqlStore) { + t.Run("should return a custom category with its Channels field set", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + channelId1 := model.NewId() + channelId2 := model.NewId() + channelId3 := model.NewId() + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + // Create a category and assign some channels to it + created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: userId, + TeamId: team.Id, + DisplayName: model.NewId(), + }, + Channels: []string{channelId1, channelId2, channelId3}, + }, + opts, + ) + require.NoError(t, err) + require.NotNil(t, created) + + // Ensure that they're returned in order + res2, err := ss.Channel().GetSidebarCategory(created.Id, opts) + assert.NoError(t, err) + assert.Equal(t, created.Id, res2.Id) + assert.Equal(t, model.SidebarCategoryCustom, res2.Type) + assert.Equal(t, created.DisplayName, res2.DisplayName) + assert.Equal(t, []string{channelId1, channelId2, channelId3}, res2.Channels) + }) + + t.Run("should return any orphaned channels with the Channels category", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create the initial categories and find the channels category + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + + // Join some channels + channel1, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel1", + DisplayName: "DEF", + TeamId: team.Id, + Type: model.ChannelTypePrivate, + }, 10) + require.NoError(t, nErr) + _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, nErr) + + channel2, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel2", + DisplayName: "ABC", + TeamId: team.Id, + Type: model.ChannelTypeOpen, + }, 10) + require.NoError(t, nErr) + _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel2.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, nErr) + + // Confirm that they're not in the Channels category in the DB + var count int64 + countErr := s.GetMasterX().Get(&count, ` + SELECT + COUNT(*) + FROM + SidebarChannels + WHERE + CategoryId = ?`, channelsCategory.Id) + require.NoError(t, countErr) + assert.Equal(t, int64(0), count) + + // Ensure that the Channels are returned in alphabetical order + res2, err := ss.Channel().GetSidebarCategory(channelsCategory.Id, opts) + assert.NoError(t, err) + assert.Equal(t, channelsCategory.Id, res2.Id) + assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + assert.Equal(t, []string{channel2.Id, channel1.Id}, res2.Channels) + }) + + t.Run("shouldn't return orphaned channels on another team with the Channels category", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create the initial categories and find the channels category + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type) + + channelsCategory := categories.Categories[1] + + // Join a channel on another team + channel1, nErr := ss.Channel().Save(&model.Channel{ + Name: "abc", + TeamId: model.NewId(), + Type: model.ChannelTypeOpen, + }, 10) + require.NoError(t, nErr) + defer ss.Channel().PermanentDelete(channel1.Id) + + _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, nErr) + + // Ensure that no channels are returned + res2, err := ss.Channel().GetSidebarCategory(channelsCategory.Id, opts) + assert.NoError(t, err) + assert.Equal(t, channelsCategory.Id, res2.Id) + assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + assert.Len(t, res2.Channels, 0) + }) + + t.Run("shouldn't return non-orphaned channels with the Channels category", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + // Create the initial categories and find the channels category + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + + // Join some channels + channel1, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel1", + DisplayName: "DEF", + TeamId: team.Id, + Type: model.ChannelTypePrivate, + }, 10) + require.NoError(t, nErr) + _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, nErr) + + channel2, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel2", + DisplayName: "ABC", + TeamId: team.Id, + Type: model.ChannelTypeOpen, + }, 10) + require.NoError(t, nErr) + _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel2.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, nErr) + + // And assign one to another category + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{channel2.Id}, + }, + }, + opts, + ) + require.NoError(t, err) + + // Ensure that the correct channel is returned in the Channels category + res2, err := ss.Channel().GetSidebarCategory(channelsCategory.Id, opts) + assert.NoError(t, err) + assert.Equal(t, channelsCategory.Id, res2.Id) + assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + assert.Equal(t, []string{channel1.Id}, res2.Channels) + }) + + t.Run("should return any orphaned DM channels with the Direct Messages category", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create the initial categories and find the DMs category + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) + + dmsCategory := categories.Categories[2] + + // Create a DM + otherUserId := model.NewId() + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.ChannelTypeDirect, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.NoError(t, nErr) + + // Ensure that the DM is returned + res2, err := ss.Channel().GetSidebarCategory(dmsCategory.Id, opts) + assert.NoError(t, err) + assert.Equal(t, dmsCategory.Id, res2.Id) + assert.Equal(t, model.SidebarCategoryDirectMessages, res2.Type) + assert.Equal(t, []string{dmChannel.Id}, res2.Channels) + }) + + t.Run("should return any orphaned GM channels with the Direct Messages category", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create the initial categories and find the DMs category + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) + + dmsCategory := categories.Categories[2] + + // Create a GM + gmChannel, nErr := ss.Channel().Save(&model.Channel{ + Name: "abc", + TeamId: "", + Type: model.ChannelTypeGroup, + }, 10) + require.NoError(t, nErr) + defer ss.Channel().PermanentDelete(gmChannel.Id) + _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: gmChannel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, nErr) + + // Ensure that the DM is returned + res2, err := ss.Channel().GetSidebarCategory(dmsCategory.Id, opts) + assert.NoError(t, err) + assert.Equal(t, dmsCategory.Id, res2.Id) + assert.Equal(t, model.SidebarCategoryDirectMessages, res2.Type) + assert.Equal(t, []string{gmChannel.Id}, res2.Channels) + }) + + t.Run("should return orphaned DM channels in the DMs category which are in a custom category on another team", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create the initial categories and find the DMs category + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) + + dmsCategory := categories.Categories[2] + + // Create a DM + otherUserId := model.NewId() + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.ChannelTypeDirect, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.NoError(t, nErr) + + // Create another team and assign the DM to a custom category on that team + otherTeam := setupTeam(t, ss, userId) + opts = &store.SidebarCategorySearchOpts{ + TeamID: otherTeam.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + _, err = ss.Channel().CreateSidebarCategory(userId, otherTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: userId, + TeamId: team.Id, + }, + Channels: []string{dmChannel.Id}, + }, + opts, + ) + require.NoError(t, err) + + // Ensure that the DM is returned with the DMs category on the original team + res2, err := ss.Channel().GetSidebarCategory(dmsCategory.Id, opts) + assert.NoError(t, err) + assert.Equal(t, dmsCategory.Id, res2.Id) + assert.Equal(t, model.SidebarCategoryDirectMessages, res2.Type) + assert.Equal(t, []string{dmChannel.Id}, res2.Channels) + }) + + t.Run("should return any orphaned DM Bot channels with the Apps category", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create the initial categories and find the DMs category + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Equal(t, model.SidebarCategoryApps, categories.Categories[3].Type) + + appsCategory := categories.Categories[3] + + // Create a Bot user + botId := model.NewId() + botUser, _ := makeBotWithUser(t, ss, &model.Bot{ + Username: botId + "somebot", + Description: "a bot", + OwnerId: botId, + LastIconUpdate: model.GetMillis(), + }) + + // Create a DM with Bot + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, botUser.UserId), + Type: model.ChannelTypeDirect, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: botUser.UserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.NoError(t, nErr) + + // Ensure that the DM Bot is returned + res2, err := ss.Channel().GetSidebarCategory(appsCategory.Id, opts) + assert.NoError(t, err) + assert.Equal(t, appsCategory.Id, res2.Id) + assert.Equal(t, model.SidebarCategoryApps, res2.Type) + assert.Equal(t, []string{dmChannel.Id}, res2.Channels) + }) + + t.Run("should return orphaned DM Bot channels in the Apps category which are in a custom category on another team", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create the initial categories and find the DMs category + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Equal(t, model.SidebarCategoryApps, categories.Categories[3].Type) + + appsCategory := categories.Categories[3] + + // Create a Bot user + botId := model.NewId() + botUser, _ := makeBotWithUser(t, ss, &model.Bot{ + Username: botId + "somebot", + Description: "a bot", + OwnerId: botId, + LastIconUpdate: model.GetMillis(), + }) + + // Create a DM with Bot + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, botUser.UserId), + Type: model.ChannelTypeDirect, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: botUser.UserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.NoError(t, nErr) + + // Create another team and assign the DM Bot to a custom category on that team + otherTeam := setupTeam(t, ss, userId) + opts = &store.SidebarCategorySearchOpts{ + TeamID: otherTeam.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + _, err = ss.Channel().CreateSidebarCategory(userId, otherTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: userId, + TeamId: team.Id, + }, + Channels: []string{dmChannel.Id}, + }, + opts, + ) + require.NoError(t, err) + + // Ensure that the DM is returned with the DMs category on the original team + res2, err := ss.Channel().GetSidebarCategory(appsCategory.Id, opts) + assert.NoError(t, err) + assert.Equal(t, appsCategory.Id, res2.Id) + assert.Equal(t, model.SidebarCategoryApps, res2.Type) + assert.Equal(t, []string{dmChannel.Id}, res2.Channels) + }) +} + +func testGetSidebarCategoriesWithAppsCategory(t *testing.T, ss store.Store) { + t.Run("should return channels in the same order between different ways of getting categories", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + channelIds := []string{ + model.NewId(), + model.NewId(), + model.NewId(), + } + + newCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ + Channels: channelIds, + }, + opts, + ) + require.NoError(t, err) + require.NotNil(t, newCategory) + + gotCategory, err := ss.Channel().GetSidebarCategory(newCategory.Id, opts) + require.NoError(t, err) + + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Len(t, res.Categories, 5) + + require.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type) + + // This looks unnecessary, but I was getting different results from some of these before + assert.Equal(t, newCategory.Channels, res.Categories[1].Channels) + assert.Equal(t, gotCategory.Channels, res.Categories[1].Channels) + assert.Equal(t, channelIds, res.Categories[1].Channels) + }) + t.Run("should not return categories for teams deleted, or no longer a member", func(t *testing.T) { + userId := model.NewId() + + teamMember1 := setupTeam(t, ss, userId) + teamMember2 := setupTeam(t, ss, userId) + teamDeleted := setupTeam(t, ss, userId) + teamDeleted.DeleteAt = model.GetMillis() + ss.Team().Update(teamDeleted) + teamNotMember := setupTeam(t, ss) + teamDeletedMember := setupTeam(t, ss, userId) + + members, err := ss.Team().GetMembersByIds(teamDeletedMember.Id, []string{userId}, nil) + require.NoError(t, err) + require.NotEmpty(t, members) + member := members[0] + member.DeleteAt = model.GetMillis() + ss.Team().UpdateMember(member) + + teamIds := []string{ + teamMember1.Id, + teamMember2.Id, + teamDeleted.Id, + teamNotMember.Id, + teamDeletedMember.Id, + } + + for _, id := range teamIds { + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, &store.SidebarCategorySearchOpts{TeamID: id, AppsCategoryEnabled: true}) + require.NoError(t, nErr) + require.NotEmpty(t, res) + } + + opts := &store.SidebarCategorySearchOpts{ + TeamID: teamMember1.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + + // Team member and not exclude + res, err := ss.Channel().GetSidebarCategories(userId, opts) + require.NoError(t, err) + assert.Equal(t, 4, len(res.Categories)) + + // No team member and not exclude + opts.TeamID = teamDeleted.Id + res, err = ss.Channel().GetSidebarCategories(userId, opts) + require.NoError(t, err) + assert.Equal(t, 0, len(res.Categories)) + + // No team member and exclude + opts.ExcludeTeam = true + res, err = ss.Channel().GetSidebarCategories(userId, opts) + require.NoError(t, err) + assert.Equal(t, 8, len(res.Categories)) + + // Team member and exclude + opts.TeamID = teamMember1.Id + res, err = ss.Channel().GetSidebarCategories(userId, opts) + require.NoError(t, err) + assert.Equal(t, 4, len(res.Categories)) + }) +} + +func testUpdateSidebarCategoriesWithAppsCategory(t *testing.T, ss store.Store) { + t.Run("ensure the query to update SidebarCategories hasn't been polluted by UpdateSidebarCategoryOrder", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create the initial categories + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, err) + require.NotEmpty(t, res) + + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + + favoritesCategory := initialCategories.Categories[0] + channelsCategory := initialCategories.Categories[1] + dmsCategory := initialCategories.Categories[2] + appsCategory := initialCategories.Categories[3] + + // And then update one of them + updated, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + channelsCategory, + }, + opts, + ) + require.NoError(t, err) + assert.Equal(t, channelsCategory, updated[0]) + assert.Equal(t, "Channels", updated[0].DisplayName) + + // And then reorder the categories + err = ss.Channel().UpdateSidebarCategoryOrder(userId, team.Id, []string{dmsCategory.Id, favoritesCategory.Id, channelsCategory.Id, appsCategory.Id}) + require.NoError(t, err) + + // Which somehow blanks out stuff because ??? + got, err := ss.Channel().GetSidebarCategory(favoritesCategory.Id, opts) + require.NoError(t, err) + assert.Equal(t, "Favorites", got.DisplayName) + }) + + t.Run("categories should be returned in their original order", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create the initial categories + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, err) + require.NotEmpty(t, res) + + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + + favoritesCategory := initialCategories.Categories[0] + channelsCategory := initialCategories.Categories[1] + dmsCategory := initialCategories.Categories[2] + appsCategory := initialCategories.Categories[3] + + // And then update them + updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + favoritesCategory, + channelsCategory, + dmsCategory, + appsCategory, + }) + assert.NoError(t, err) + assert.Equal(t, favoritesCategory.Id, updatedCategories[0].Id) + assert.Equal(t, channelsCategory.Id, updatedCategories[1].Id) + assert.Equal(t, dmsCategory.Id, updatedCategories[2].Id) + assert.Equal(t, appsCategory.Id, updatedCategories[3].Id) + }) + + t.Run("should silently fail to update read only fields", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + + favoritesCategory := initialCategories.Categories[0] + channelsCategory := initialCategories.Categories[1] + dmsCategory := initialCategories.Categories[2] + appsCategory := initialCategories.Categories[3] + + customCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{}, opts) + require.NoError(t, err) + + categoriesToUpdate := []*model.SidebarCategoryWithChannels{ + // Try to change the type of Favorites + { + SidebarCategory: model.SidebarCategory{ + Id: favoritesCategory.Id, + DisplayName: "something else", + }, + Channels: favoritesCategory.Channels, + }, + // Try to change the type of Channels + { + SidebarCategory: model.SidebarCategory{ + Id: channelsCategory.Id, + Type: model.SidebarCategoryDirectMessages, + }, + Channels: channelsCategory.Channels, + }, + // Try to change the Channels of DMs + { + SidebarCategory: dmsCategory.SidebarCategory, + Channels: []string{"fakechannel"}, + }, + // Try to change the Channels of Apps + { + SidebarCategory: appsCategory.SidebarCategory, + Channels: []string{"fakechannel"}, + }, + // Try to change the UserId/TeamId of a custom category + { + SidebarCategory: model.SidebarCategory{ + Id: customCategory.Id, + UserId: model.NewId(), + TeamId: model.NewId(), + Sorting: customCategory.Sorting, + DisplayName: customCategory.DisplayName, + }, + Channels: customCategory.Channels, + }, + } + + updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate, opts) + assert.NoError(t, err) + + assert.NotEqual(t, "Favorites", categoriesToUpdate[0].DisplayName) + assert.Equal(t, "Favorites", updatedCategories[0].DisplayName) + assert.NotEqual(t, model.SidebarCategoryChannels, categoriesToUpdate[1].Type) + assert.Equal(t, model.SidebarCategoryChannels, updatedCategories[1].Type) + assert.NotEqual(t, []string{}, categoriesToUpdate[2].Channels) + assert.Equal(t, []string{}, updatedCategories[2].Channels) + assert.NotEqual(t, []string{}, categoriesToUpdate[3].Channels) + assert.Equal(t, []string{}, updatedCategories[3].Channels) + assert.NotEqual(t, userId, categoriesToUpdate[4].UserId) + assert.Equal(t, userId, updatedCategories[4].UserId) + }) + + t.Run("should add and remove favorites preferences based on the Favorites category", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create the initial categories and find the favorites category + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + + // Join a channel + channel, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.ChannelTypeOpen, + TeamId: team.Id, + }, 10) + require.NoError(t, nErr) + _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, nErr) + + // Assign it to favorites + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + }, + opts, + ) + assert.NoError(t, err) + + res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id) + assert.NoError(t, nErr) + assert.NotNil(t, res2) + assert.Equal(t, "true", res2.Value) + + // And then remove it + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + }, + opts, + ) + assert.NoError(t, err) + + res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id) + assert.Error(t, nErr) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res2) + }) + + t.Run("should add and remove favorites preferences for DMs", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create the initial categories and find the favorites category + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + + // Create a direct channel + otherUserId := model.NewId() + + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.ChannelTypeDirect, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + assert.NoError(t, nErr) + + // Assign it to favorites + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + }, + opts, + ) + assert.NoError(t, err) + + res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id) + assert.NoError(t, nErr) + assert.NotNil(t, res2) + assert.Equal(t, "true", res2.Value) + + // And then remove it + dmsCategory := categories.Categories[2] + require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type) + + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: dmsCategory.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + }, + opts, + ) + assert.NoError(t, err) + + res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id) + assert.Error(t, nErr) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res2) + }) + + t.Run("should add and remove favorites preferences, even if the channel is already favorited in preferences", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + team2 := setupTeam(t, ss, userId) + + // Create the initial categories and find the favorites categories in each team + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + + opts = &store.SidebarCategorySearchOpts{ + TeamID: team2.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id, opts) + require.NoError(t, err) + + favoritesCategory2 := categories2.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory2.Type) + + // Create a direct channel + otherUserId := model.NewId() + + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.ChannelTypeDirect, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + assert.NoError(t, nErr) + + // Assign it to favorites on the first team. The favorites preference gets set for all teams. + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + }, + opts, + ) + assert.NoError(t, err) + + res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id) + assert.NoError(t, nErr) + assert.NotNil(t, res2) + assert.Equal(t, "true", res2.Value) + + // Assign it to favorites on the second team. The favorites preference is already set. + updated, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory2.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + }, + opts, + ) + assert.NoError(t, err) + assert.Equal(t, []string{dmChannel.Id}, updated[0].Channels) + + res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id) + assert.NoError(t, nErr) + assert.NotNil(t, res2) + assert.Equal(t, "true", res2.Value) + + // Remove it from favorites on the first team. This clears the favorites preference for all teams. + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{}, + }, + }, + opts, + ) + assert.NoError(t, err) + + res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id) + require.Error(t, nErr) + assert.Nil(t, res2) + + // Remove it from favorites on the second team. The favorites preference was already deleted. + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory2.SidebarCategory, + Channels: []string{}, + }, + }, + opts, + ) + assert.NoError(t, err) + + res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id) + require.Error(t, nErr) + assert.Nil(t, res2) + }) + + t.Run("should not affect other users' favorites preferences", func(t *testing.T) { + userId := model.NewId() + userId2 := model.NewId() + team := setupTeam(t, ss, userId, userId2) + + // Create the initial categories and find the favorites category + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + + // Create the other users' categories + res, nErr = ss.Channel().CreateInitialSidebarCategories(userId2, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId2, team.Id, opts) + require.NoError(t, err) + + favoritesCategory2 := categories2.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory2.Type) + channelsCategory2 := categories2.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory2.Type) + + // Have both users join a channel + channel, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.ChannelTypeOpen, + TeamId: team.Id, + }, 10) + require.NoError(t, nErr) + _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, nErr) + _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId2, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, nErr) + + // Have user1 favorite it + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{}, + }, + }, + opts, + ) + assert.NoError(t, err) + + res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id) + assert.NoError(t, nErr) + assert.NotNil(t, res2) + assert.Equal(t, "true", res2.Value) + + res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res2) + + // And user2 favorite it + _, _, err = ss.Channel().UpdateSidebarCategories(userId2, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory2.SidebarCategory, + Channels: []string{channel.Id}, + }, + { + SidebarCategory: channelsCategory2.SidebarCategory, + Channels: []string{}, + }, + }, + opts, + ) + assert.NoError(t, err) + + res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id) + assert.NoError(t, nErr) + assert.NotNil(t, res2) + assert.Equal(t, "true", res2.Value) + + res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id) + assert.NoError(t, nErr) + assert.NotNil(t, res2) + assert.Equal(t, "true", res2.Value) + + // And then user1 unfavorite it + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{}, + }, + }, + opts, + ) + assert.NoError(t, err) + + res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res2) + + res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id) + assert.NoError(t, nErr) + assert.NotNil(t, res2) + assert.Equal(t, "true", res2.Value) + + // And finally user2 favorite it + _, _, err = ss.Channel().UpdateSidebarCategories(userId2, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory2.SidebarCategory, + Channels: []string{channel.Id}, + }, + { + SidebarCategory: favoritesCategory2.SidebarCategory, + Channels: []string{}, + }, + }, + opts, + ) + assert.NoError(t, err) + + res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res2) + + res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res2) + }) + + t.Run("channels removed from Channels or DMs categories should be re-added", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Create some channels + channel, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.ChannelTypeOpen, + TeamId: team.Id, + }, 10) + require.NoError(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, err) + + otherUserId := model.NewId() + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.ChannelTypeDirect, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.NoError(t, nErr) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + // And some categories + initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, nErr) + + channelsCategory := initialCategories.Categories[1] + dmsCategory := initialCategories.Categories[2] + + require.Equal(t, []string{channel.Id}, channelsCategory.Channels) + require.Equal(t, []string{dmChannel.Id}, dmsCategory.Channels) + + // Try to save the categories with no channels in them + categoriesToUpdate := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{}, + }, + { + SidebarCategory: dmsCategory.SidebarCategory, + Channels: []string{}, + }, + } + + updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate, opts) + assert.NoError(t, nErr) + + // The channels should still exist in the category because they would otherwise be orphaned + assert.Equal(t, []string{channel.Id}, updatedCategories[0].Channels) + assert.Equal(t, []string{dmChannel.Id}, updatedCategories[1].Channels) + }) + + t.Run("should be able to move DMs into and out of custom categories", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + otherUserId := model.NewId() + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.ChannelTypeDirect, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.NoError(t, nErr) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + // The DM should start in the DMs category + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + + dmsCategory := initialCategories.Categories[2] + require.Equal(t, []string{dmChannel.Id}, dmsCategory.Channels) + + // Now move the DM into a custom category + customCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{}, opts) + require.NoError(t, err) + + categoriesToUpdate := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: dmsCategory.SidebarCategory, + Channels: []string{}, + }, + { + SidebarCategory: customCategory.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + } + + updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate) + assert.NoError(t, err) + assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id) + assert.Equal(t, []string{}, updatedCategories[0].Channels) + assert.Equal(t, customCategory.Id, updatedCategories[1].Id) + assert.Equal(t, []string{dmChannel.Id}, updatedCategories[1].Channels) + + updatedDmsCategory, err := ss.Channel().GetSidebarCategory(dmsCategory.Id, opts) + require.NoError(t, err) + assert.Equal(t, []string{}, updatedDmsCategory.Channels) + + updatedCustomCategory, err := ss.Channel().GetSidebarCategory(customCategory.Id, opts) + require.NoError(t, err) + assert.Equal(t, []string{dmChannel.Id}, updatedCustomCategory.Channels) + + // And move it back out of the custom category + categoriesToUpdate = []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: dmsCategory.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + { + SidebarCategory: customCategory.SidebarCategory, + Channels: []string{}, + }, + } + + updatedCategories, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate, opts) + assert.NoError(t, err) + assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id) + assert.Equal(t, []string{dmChannel.Id}, updatedCategories[0].Channels) + assert.Equal(t, customCategory.Id, updatedCategories[1].Id) + assert.Equal(t, []string{}, updatedCategories[1].Channels) + + updatedDmsCategory, err = ss.Channel().GetSidebarCategory(dmsCategory.Id, opts) + require.NoError(t, err) + assert.Equal(t, []string{dmChannel.Id}, updatedDmsCategory.Channels) + + updatedCustomCategory, err = ss.Channel().GetSidebarCategory(customCategory.Id, opts) + require.NoError(t, err) + assert.Equal(t, []string{}, updatedCustomCategory.Channels) + }) + + t.Run("should successfully move channels between categories", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Join a channel + channel, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.ChannelTypeOpen, + TeamId: team.Id, + }, 10) + require.NoError(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, err) + + // And then create the initial categories so that it includes the channel + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, nErr) + + channelsCategory := initialCategories.Categories[1] + require.Equal(t, []string{channel.Id}, channelsCategory.Channels) + + customCategory, nErr := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{}, opts) + require.NoError(t, nErr) + + // Move the channel one way + updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{}, + }, + { + SidebarCategory: customCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + }, + opts, + ) + assert.NoError(t, nErr) + + assert.Equal(t, []string{}, updatedCategories[0].Channels) + assert.Equal(t, []string{channel.Id}, updatedCategories[1].Channels) + + // And then the other + updatedCategories, _, nErr = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + { + SidebarCategory: customCategory.SidebarCategory, + Channels: []string{}, + }, + }, + opts, + ) + assert.NoError(t, nErr) + assert.Equal(t, []string{channel.Id}, updatedCategories[0].Channels) + assert.Equal(t, []string{}, updatedCategories[1].Channels) + }) + + t.Run("should correctly return the original categories that were modified", func(t *testing.T) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + // Join a channel + channel, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.ChannelTypeOpen, + TeamId: team.Id, + }, 10) + require.NoError(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, err) + + // And then create the initial categories so that Channels includes the channel + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, nErr) + + channelsCategory := initialCategories.Categories[1] + require.Equal(t, []string{channel.Id}, channelsCategory.Channels) + + customCategory, nErr := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "originalName", + }, + }, + opts, + ) + require.NoError(t, nErr) + + // Rename the custom category + updatedCategories, originalCategories, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: customCategory.Id, + DisplayName: "updatedName", + }, + }, + }, + opts, + ) + require.NoError(t, nErr) + require.Equal(t, len(updatedCategories), len(originalCategories)) + assert.Equal(t, "originalName", originalCategories[0].DisplayName) + assert.Equal(t, "updatedName", updatedCategories[0].DisplayName) + + // Move a channel + updatedCategories, originalCategories, nErr = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{}, + }, + { + SidebarCategory: customCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + }, + opts, + ) + require.NoError(t, nErr) + require.Equal(t, len(updatedCategories), len(originalCategories)) + require.Equal(t, updatedCategories[0].Id, originalCategories[0].Id) + require.Equal(t, updatedCategories[1].Id, originalCategories[1].Id) + + assert.Equal(t, []string{channel.Id}, originalCategories[0].Channels) + assert.Equal(t, []string{}, updatedCategories[0].Channels) + assert.Equal(t, []string{}, originalCategories[1].Channels) + assert.Equal(t, []string{channel.Id}, updatedCategories[1].Channels) + }) +} + +func setupInitialSidebarCategoriesWithAppsCategory(t *testing.T, ss store.Store) (string, string) { + userId := model.NewId() + team := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id, opts) + require.NoError(t, err) + require.Len(t, res.Categories, 4) + + return userId, team.Id +} + +func testClearSidebarOnTeamLeaveWithAppsCategory(t *testing.T, ss store.Store, s SqlStore) { + t.Run("should delete all sidebar categories and channels on the team", func(t *testing.T) { + userId, teamId := setupInitialSidebarCategoriesWithAppsCategory(t, ss) + + user := &model.User{ + Id: userId, + } + + // Create some channels and assign them to a custom category + channel1, nErr := ss.Channel().Save(&model.Channel{ + Name: model.NewId(), + TeamId: teamId, + Type: model.ChannelTypeOpen, + }, 1000) + require.NoError(t, nErr) + + dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, &model.User{ + Id: model.NewId(), + }) + require.NoError(t, nErr) + + opts := &store.SidebarCategorySearchOpts{ + AppsCategoryEnabled: true, + } + + _, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + Channels: []string{channel1.Id, dmChannel1.Id}, + }, + opts, + ) + require.NoError(t, err) + + // Confirm that we start with the right number of categories and SidebarChannels entries + var count int64 + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId) + require.NoError(t, err) + require.Equal(t, int64(5), count) + + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId) + require.NoError(t, err) + require.Equal(t, int64(2), count) + + // Leave the team + err = ss.Channel().ClearSidebarOnTeamLeave(userId, teamId) + assert.NoError(t, err) + + // Confirm that all the categories and SidebarChannel entries have been deleted + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId) + require.NoError(t, err) + assert.Equal(t, int64(0), count) + + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId) + require.NoError(t, err) + assert.Equal(t, int64(0), count) + }) + + t.Run("should not delete sidebar categories and channels on another the team", func(t *testing.T) { + userId, teamId := setupInitialSidebarCategoriesWithAppsCategory(t, ss) + + user := &model.User{ + Id: userId, + } + + // Create some channels and assign them to a custom category + channel1, nErr := ss.Channel().Save(&model.Channel{ + Name: model.NewId(), + TeamId: teamId, + Type: model.ChannelTypeOpen, + }, 1000) + require.NoError(t, nErr) + + dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, &model.User{ + Id: model.NewId(), + }) + require.NoError(t, nErr) + + opts := &store.SidebarCategorySearchOpts{ + AppsCategoryEnabled: true, + } + + _, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + Channels: []string{channel1.Id, dmChannel1.Id}, + }, + opts, + ) + require.NoError(t, err) + + // Confirm that we start with the right number of categories and SidebarChannels entries + var count int64 + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId) + require.NoError(t, err) + require.Equal(t, int64(5), count) + + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId) + require.NoError(t, err) + require.Equal(t, int64(2), count) + + // Leave another team + err = ss.Channel().ClearSidebarOnTeamLeave(userId, model.NewId()) + assert.NoError(t, err) + + // Confirm that nothing has been deleted + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId) + require.NoError(t, err) + assert.Equal(t, int64(5), count) + + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId) + require.NoError(t, err) + assert.Equal(t, int64(2), count) + }) + + t.Run("MM-30314 should not delete channels on another team under specific circumstances", func(t *testing.T) { + userId, teamId := setupInitialSidebarCategoriesWithAppsCategory(t, ss) + + user := &model.User{ + Id: userId, + } + user2 := &model.User{ + Id: model.NewId(), + } + + // Create a second team and set up the sidebar categories for it + team2 := setupTeam(t, ss, userId) + + opts := &store.SidebarCategorySearchOpts{ + TeamID: team2.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, err) + require.NotEmpty(t, res) + + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id, opts) + require.NoError(t, err) + require.Len(t, res.Categories, 4) + + // On the first team, create some channels and assign them to a custom category + channel1, nErr := ss.Channel().Save(&model.Channel{ + Name: model.NewId(), + TeamId: teamId, + Type: model.ChannelTypeOpen, + }, 1000) + require.NoError(t, nErr) + + dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, user2) + require.NoError(t, nErr) + + _, err = ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + Channels: []string{channel1.Id, dmChannel1.Id}, + }, + opts, + ) + require.NoError(t, err) + + // Do the same on the second team + channel2, nErr := ss.Channel().Save(&model.Channel{ + Name: model.NewId(), + TeamId: team2.Id, + Type: model.ChannelTypeOpen, + }, 1000) + require.NoError(t, nErr) + + _, err = ss.Channel().CreateSidebarCategory(userId, team2.Id, &model.SidebarCategoryWithChannels{ + Channels: []string{channel2.Id, dmChannel1.Id}, + }, + opts, + ) + require.NoError(t, err) + + // Confirm that we start with the right number of categories and SidebarChannels entries + var count int64 + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId) + require.NoError(t, err) + require.Equal(t, int64(10), count) + + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId) + require.NoError(t, err) + require.Equal(t, int64(4), count) + + // Leave the first team + err = ss.Channel().ClearSidebarOnTeamLeave(userId, teamId) + assert.NoError(t, err) + + // Confirm that we have the correct number of categories and SidebarChannels entries left over + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId) + require.NoError(t, err) + assert.Equal(t, int64(5), count) + + err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId) + require.NoError(t, err) + assert.Equal(t, int64(2), count) + + // Confirm that the categories on the second team are unchanged + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id, &store.SidebarCategorySearchOpts{ + AppsCategoryEnabled: true, + }) + require.NoError(t, err) + assert.Len(t, res.Categories, 5) + + assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type) + assert.Equal(t, []string{channel2.Id, dmChannel1.Id}, res.Categories[1].Channels) + }) +} + +func testDeleteSidebarCategoryWithAppsCategory(t *testing.T, ss store.Store, s SqlStore) { + t.Run("should correctly remove an empty category", func(t *testing.T) { + userId, teamId := setupInitialSidebarCategoriesWithAppsCategory(t, ss) + defer ss.User().PermanentDelete(userId) + + opts := &store.SidebarCategorySearchOpts{ + AppsCategoryEnabled: true, + } + + newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}, opts) + require.NoError(t, err) + require.NotNil(t, newCategory) + + // Ensure that the category was created properly + res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId, opts) + require.NoError(t, err) + require.Len(t, res.Categories, 5) + + // Then delete it and confirm that was done correctly + err = ss.Channel().DeleteSidebarCategory(newCategory.Id) + assert.NoError(t, err) + + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId, opts) + require.NoError(t, err) + require.Len(t, res.Categories, 4) + }) + + t.Run("should correctly remove a category and its channels", func(t *testing.T) { + userId, teamId := setupInitialSidebarCategoriesWithAppsCategory(t, ss) + defer ss.User().PermanentDelete(userId) + + opts := &store.SidebarCategorySearchOpts{ + AppsCategoryEnabled: true, + } + + user := &model.User{ + Id: userId, + } + + // Create some channels + channel1, nErr := ss.Channel().Save(&model.Channel{ + Name: model.NewId(), + TeamId: teamId, + Type: model.ChannelTypeOpen, + }, 1000) + require.NoError(t, nErr) + defer ss.Channel().PermanentDelete(channel1.Id) + + channel2, nErr := ss.Channel().Save(&model.Channel{ + Name: model.NewId(), + TeamId: teamId, + Type: model.ChannelTypePrivate, + }, 1000) + require.NoError(t, nErr) + defer ss.Channel().PermanentDelete(channel2.Id) + + dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, &model.User{ + Id: model.NewId(), + }) + require.NoError(t, nErr) + defer ss.Channel().PermanentDelete(dmChannel1.Id) + + // Assign some of those channels to a custom category + newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + Channels: []string{channel1.Id, channel2.Id, dmChannel1.Id}, + }, + opts, + ) + require.NoError(t, err) + require.NotNil(t, newCategory) + + // Ensure that the categories are set up correctly + res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId, opts) + require.NoError(t, err) + require.Len(t, res.Categories, 5) + + require.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type) + require.Equal(t, []string{channel1.Id, channel2.Id, dmChannel1.Id}, res.Categories[1].Channels) + + // Actually delete the channel + err = ss.Channel().DeleteSidebarCategory(newCategory.Id) + assert.NoError(t, err) + + // Confirm that the category was deleted... + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId, opts) + assert.NoError(t, err) + assert.Len(t, res.Categories, 4) + + // ...and that the corresponding SidebarChannel entries were deleted + var count int64 + countErr := s.GetMasterX().Get(&count, ` + SELECT + COUNT(*) + FROM + SidebarChannels + WHERE + CategoryId = ?`, newCategory.Id) + require.NoError(t, countErr) + assert.Equal(t, int64(0), count) + }) + + t.Run("should not allow you to remove non-custom categories", func(t *testing.T) { + userId, teamId := setupInitialSidebarCategoriesWithAppsCategory(t, ss) + defer ss.User().PermanentDelete(userId) + res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId, &store.SidebarCategorySearchOpts{ + AppsCategoryEnabled: true, + }) + require.NoError(t, err) + require.Len(t, res.Categories, 4) + require.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) + require.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) + require.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) + require.Equal(t, model.SidebarCategoryApps, res.Categories[3].Type) + + err = ss.Channel().DeleteSidebarCategory(res.Categories[0].Id) + assert.Error(t, err) + + err = ss.Channel().DeleteSidebarCategory(res.Categories[1].Id) + assert.Error(t, err) + + err = ss.Channel().DeleteSidebarCategory(res.Categories[2].Id) + assert.Error(t, err) + + err = ss.Channel().DeleteSidebarCategory(res.Categories[3].Id) + assert.Error(t, err) + }) +} + +func testUpdateSidebarChannelsByPreferencesWithAppsCategory(t *testing.T, ss store.Store) { + t.Run("Should be able to update sidebar channels", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + opts := &store.SidebarCategorySearchOpts{ + TeamID: teamId, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + require.NoError(t, nErr) + require.NotEmpty(t, res) + + channel, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.ChannelTypeOpen, + TeamId: teamId, + }, 10) + require.NoError(t, nErr) + + err := ss.Channel().UpdateSidebarChannelsByPreferences(model.Preferences{ + model.Preference{ + Name: channel.Id, + Category: model.PreferenceCategoryFavoriteChannel, + Value: "true", + }, + }) + assert.NoError(t, err) + }) + + t.Run("Should not panic if channel is not found", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + opts := &store.SidebarCategorySearchOpts{ + TeamID: teamId, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) + assert.NoError(t, nErr) + require.NotEmpty(t, res) + + require.NotPanics(t, func() { + _ = ss.Channel().UpdateSidebarChannelsByPreferences(model.Preferences{ + model.Preference{ + Name: "fakeid", + Category: model.PreferenceCategoryFavoriteChannel, + Value: "true", + }, + }) + }) + }) +} + +func testSidebarCategoryDeadlockWithAppsCategory(t *testing.T, ss store.Store) { + userID := model.NewId() + team := setupTeam(t, ss, userID) + + // Join a channel + channel, err := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.ChannelTypeOpen, + TeamId: team.Id, + }, 10) + require.NoError(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userID, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, err) + + // And then create the initial categories so that it includes the channel + opts := &store.SidebarCategorySearchOpts{ + TeamID: team.Id, + ExcludeTeam: false, + AppsCategoryEnabled: true, + } + res, err := ss.Channel().CreateInitialSidebarCategories(userID, opts) + require.NoError(t, err) + require.NotEmpty(t, res) + + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userID, team.Id, opts) + require.NoError(t, err) + + channelsCategory := initialCategories.Categories[1] + require.Equal(t, []string{channel.Id}, channelsCategory.Channels) + + customCategory, err := ss.Channel().CreateSidebarCategory(userID, team.Id, &model.SidebarCategoryWithChannels{}, opts) + require.NoError(t, err) + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + _, _, err := ss.Channel().UpdateSidebarCategories(userID, team.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{}, + }, + { + SidebarCategory: customCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + }) + if err != nil { + var nfErr *store.ErrNotFound + require.True(t, errors.As(err, &nfErr)) + } + }() + + go func() { + defer wg.Done() + err := ss.Channel().DeleteSidebarCategory(customCategory.Id) + require.NoError(t, err) + }() + + wg.Wait() +} diff --git a/server/channels/store/storetest/mocks/ChannelStore.go b/server/channels/store/storetest/mocks/ChannelStore.go index efff8cd0ce5..47257857855 100644 --- a/server/channels/store/storetest/mocks/ChannelStore.go +++ b/server/channels/store/storetest/mocks/ChannelStore.go @@ -298,25 +298,32 @@ func (_m *ChannelStore) CreateInitialSidebarCategories(userID string, opts *stor return r0, r1 } -// CreateSidebarCategory provides a mock function with given fields: userID, teamID, newCategory -func (_m *ChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) { - ret := _m.Called(userID, teamID, newCategory) +// CreateSidebarCategory provides a mock function with given fields: userID, teamID, newCategory, options +func (_m *ChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels, options ...*store.SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, userID, teamID, newCategory) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) var r0 *model.SidebarCategoryWithChannels var r1 error - if rf, ok := ret.Get(0).(func(string, string, *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error)); ok { - return rf(userID, teamID, newCategory) + if rf, ok := ret.Get(0).(func(string, string, *model.SidebarCategoryWithChannels, ...*store.SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error)); ok { + return rf(userID, teamID, newCategory, options...) } - if rf, ok := ret.Get(0).(func(string, string, *model.SidebarCategoryWithChannels) *model.SidebarCategoryWithChannels); ok { - r0 = rf(userID, teamID, newCategory) + if rf, ok := ret.Get(0).(func(string, string, *model.SidebarCategoryWithChannels, ...*store.SidebarCategorySearchOpts) *model.SidebarCategoryWithChannels); ok { + r0 = rf(userID, teamID, newCategory, options...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.SidebarCategoryWithChannels) } } - if rf, ok := ret.Get(1).(func(string, string, *model.SidebarCategoryWithChannels) error); ok { - r1 = rf(userID, teamID, newCategory) + if rf, ok := ret.Get(1).(func(string, string, *model.SidebarCategoryWithChannels, ...*store.SidebarCategorySearchOpts) error); ok { + r1 = rf(userID, teamID, newCategory, options...) } else { r1 = ret.Error(1) } @@ -598,6 +605,32 @@ func (_m *ChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterID st return r0, r1 } +// GetBotChannelsByUser provides a mock function with given fields: userID, opts +func (_m *ChannelStore) GetBotChannelsByUser(userID string, opts store.ChannelSearchOpts) (model.ChannelList, error) { + ret := _m.Called(userID, opts) + + var r0 model.ChannelList + var r1 error + if rf, ok := ret.Get(0).(func(string, store.ChannelSearchOpts) (model.ChannelList, error)); ok { + return rf(userID, opts) + } + if rf, ok := ret.Get(0).(func(string, store.ChannelSearchOpts) model.ChannelList); ok { + r0 = rf(userID, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(model.ChannelList) + } + } + + if rf, ok := ret.Get(1).(func(string, store.ChannelSearchOpts) error); ok { + r1 = rf(userID, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetByName provides a mock function with given fields: team_id, name, allowFromCache func (_m *ChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) { ret := _m.Called(team_id, name, allowFromCache) @@ -1592,25 +1625,32 @@ func (_m *ChannelStore) GetSidebarCategories(userID string, opts *store.SidebarC return r0, r1 } -// GetSidebarCategoriesForTeamForUser provides a mock function with given fields: userID, teamID -func (_m *ChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) { - ret := _m.Called(userID, teamID) +// GetSidebarCategoriesForTeamForUser provides a mock function with given fields: userID, teamID, options +func (_m *ChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string, options ...*store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, userID, teamID) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) var r0 *model.OrderedSidebarCategories var r1 error - if rf, ok := ret.Get(0).(func(string, string) (*model.OrderedSidebarCategories, error)); ok { - return rf(userID, teamID) + if rf, ok := ret.Get(0).(func(string, string, ...*store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error)); ok { + return rf(userID, teamID, options...) } - if rf, ok := ret.Get(0).(func(string, string) *model.OrderedSidebarCategories); ok { - r0 = rf(userID, teamID) + if rf, ok := ret.Get(0).(func(string, string, ...*store.SidebarCategorySearchOpts) *model.OrderedSidebarCategories); ok { + r0 = rf(userID, teamID, options...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.OrderedSidebarCategories) } } - if rf, ok := ret.Get(1).(func(string, string) error); ok { - r1 = rf(userID, teamID) + if rf, ok := ret.Get(1).(func(string, string, ...*store.SidebarCategorySearchOpts) error); ok { + r1 = rf(userID, teamID, options...) } else { r1 = ret.Error(1) } @@ -1618,25 +1658,32 @@ func (_m *ChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID return r0, r1 } -// GetSidebarCategory provides a mock function with given fields: categoryID -func (_m *ChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) { - ret := _m.Called(categoryID) +// GetSidebarCategory provides a mock function with given fields: categoryID, options +func (_m *ChannelStore) GetSidebarCategory(categoryID string, options ...*store.SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, categoryID) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) var r0 *model.SidebarCategoryWithChannels var r1 error - if rf, ok := ret.Get(0).(func(string) (*model.SidebarCategoryWithChannels, error)); ok { - return rf(categoryID) + if rf, ok := ret.Get(0).(func(string, ...*store.SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error)); ok { + return rf(categoryID, options...) } - if rf, ok := ret.Get(0).(func(string) *model.SidebarCategoryWithChannels); ok { - r0 = rf(categoryID) + if rf, ok := ret.Get(0).(func(string, ...*store.SidebarCategorySearchOpts) *model.SidebarCategoryWithChannels); ok { + r0 = rf(categoryID, options...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.SidebarCategoryWithChannels) } } - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(categoryID) + if rf, ok := ret.Get(1).(func(string, ...*store.SidebarCategorySearchOpts) error); ok { + r1 = rf(categoryID, options...) } else { r1 = ret.Error(1) } @@ -2582,34 +2629,41 @@ func (_m *ChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([ return r0, r1 } -// UpdateSidebarCategories provides a mock function with given fields: userID, teamID, categories -func (_m *ChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { - ret := _m.Called(userID, teamID, categories) +// UpdateSidebarCategories provides a mock function with given fields: userID, teamID, categories, options +func (_m *ChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels, options ...*store.SidebarCategorySearchOpts) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, userID, teamID, categories) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) var r0 []*model.SidebarCategoryWithChannels var r1 []*model.SidebarCategoryWithChannels var r2 error - if rf, ok := ret.Get(0).(func(string, string, []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error)); ok { - return rf(userID, teamID, categories) + if rf, ok := ret.Get(0).(func(string, string, []*model.SidebarCategoryWithChannels, ...*store.SidebarCategorySearchOpts) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error)); ok { + return rf(userID, teamID, categories, options...) } - if rf, ok := ret.Get(0).(func(string, string, []*model.SidebarCategoryWithChannels) []*model.SidebarCategoryWithChannels); ok { - r0 = rf(userID, teamID, categories) + if rf, ok := ret.Get(0).(func(string, string, []*model.SidebarCategoryWithChannels, ...*store.SidebarCategorySearchOpts) []*model.SidebarCategoryWithChannels); ok { + r0 = rf(userID, teamID, categories, options...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.SidebarCategoryWithChannels) } } - if rf, ok := ret.Get(1).(func(string, string, []*model.SidebarCategoryWithChannels) []*model.SidebarCategoryWithChannels); ok { - r1 = rf(userID, teamID, categories) + if rf, ok := ret.Get(1).(func(string, string, []*model.SidebarCategoryWithChannels, ...*store.SidebarCategorySearchOpts) []*model.SidebarCategoryWithChannels); ok { + r1 = rf(userID, teamID, categories, options...) } else { if ret.Get(1) != nil { r1 = ret.Get(1).([]*model.SidebarCategoryWithChannels) } } - if rf, ok := ret.Get(2).(func(string, string, []*model.SidebarCategoryWithChannels) error); ok { - r2 = rf(userID, teamID, categories) + if rf, ok := ret.Get(2).(func(string, string, []*model.SidebarCategoryWithChannels, ...*store.SidebarCategorySearchOpts) error); ok { + r2 = rf(userID, teamID, categories, options...) } else { r2 = ret.Error(2) } diff --git a/server/channels/store/timerlayer/timerlayer.go b/server/channels/store/timerlayer/timerlayer.go index 3dc9a94c19e..815b215305b 100644 --- a/server/channels/store/timerlayer/timerlayer.go +++ b/server/channels/store/timerlayer/timerlayer.go @@ -774,10 +774,10 @@ func (s *TimerLayerChannelStore) CreateInitialSidebarCategories(userID string, o return result, err } -func (s *TimerLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) { +func (s *TimerLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels, options ...*store.SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error) { start := time.Now() - result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory) + result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory, options...) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -982,6 +982,22 @@ func (s *TimerLayerChannelStore) GetAllDirectChannelsForExportAfter(limit int, a return result, err } +func (s *TimerLayerChannelStore) GetBotChannelsByUser(userID string, opts store.ChannelSearchOpts) (model.ChannelList, error) { + start := time.Now() + + result, err := s.ChannelStore.GetBotChannelsByUser(userID, opts) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetBotChannelsByUser", success, elapsed) + } + return result, err +} + func (s *TimerLayerChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) { start := time.Now() @@ -1606,10 +1622,10 @@ func (s *TimerLayerChannelStore) GetSidebarCategories(userID string, opts *store return result, err } -func (s *TimerLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) { +func (s *TimerLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string, options ...*store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) { start := time.Now() - result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID) + result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID, options...) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -1622,10 +1638,10 @@ func (s *TimerLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID strin return result, err } -func (s *TimerLayerChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) { +func (s *TimerLayerChannelStore) GetSidebarCategory(categoryID string, options ...*store.SidebarCategorySearchOpts) (*model.SidebarCategoryWithChannels, error) { start := time.Now() - result, err := s.ChannelStore.GetSidebarCategory(categoryID) + result, err := s.ChannelStore.GetSidebarCategory(categoryID, options...) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -2399,10 +2415,10 @@ func (s *TimerLayerChannelStore) UpdateMultipleMembers(members []*model.ChannelM return result, err } -func (s *TimerLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { +func (s *TimerLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels, options ...*store.SidebarCategorySearchOpts) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { start := time.Now() - result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories) + result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories, options...) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { diff --git a/server/model/channel_sidebar.go b/server/model/channel_sidebar.go index e8b5a6a215d..2e2c3612c1b 100644 --- a/server/model/channel_sidebar.go +++ b/server/model/channel_sidebar.go @@ -13,11 +13,12 @@ type SidebarCategoryType string type SidebarCategorySorting string const ( - // Each sidebar category has a 'type'. System categories are Channels, Favorites and DMs + // Each sidebar category has a 'type'. System categories are Channels, Favorites, DMs and Apps // All user-created categories will have type Custom SidebarCategoryChannels SidebarCategoryType = "channels" SidebarCategoryDirectMessages SidebarCategoryType = "direct_messages" SidebarCategoryFavorites SidebarCategoryType = "favorites" + SidebarCategoryApps SidebarCategoryType = "apps" SidebarCategoryCustom SidebarCategoryType = "custom" // Increment to use when adding/reordering things in the sidebar MinimalSidebarSortDistance = 10 @@ -25,6 +26,7 @@ const ( DefaultSidebarSortOrderFavorites = 0 DefaultSidebarSortOrderChannels = DefaultSidebarSortOrderFavorites + MinimalSidebarSortDistance DefaultSidebarSortOrderDMs = DefaultSidebarSortOrderChannels + MinimalSidebarSortDistance + DefaultSidebarSortOrderApps = DefaultSidebarSortOrderDMs + MinimalSidebarSortDistance // Sorting modes // default for all categories except DMs (behaves like manual) SidebarCategorySortDefault SidebarCategorySorting = "" @@ -36,6 +38,13 @@ const ( SidebarCategorySortAlphabetical SidebarCategorySorting = "alpha" ) +var SystemSidebarCategories = []SidebarCategoryType{ + SidebarCategoryChannels, + SidebarCategoryDirectMessages, + SidebarCategoryFavorites, + SidebarCategoryApps, +} + // SidebarCategory represents the corresponding DB table type SidebarCategory struct { Id string `json:"id"` @@ -77,7 +86,7 @@ type SidebarChannel struct { type SidebarChannels []*SidebarChannel type SidebarCategoriesWithChannels []*SidebarCategoryWithChannels -var categoryIdPattern = regexp.MustCompile("(favorites|channels|direct_messages)_[a-z0-9]{26}_[a-z0-9]{26}") +var categoryIdPattern = regexp.MustCompile("(favorites|channels|direct_messages|apps)_[a-z0-9]{26}_[a-z0-9]{26}") func IsValidCategoryId(s string) bool { // Category IDs can either be regular IDs