diff --git a/e2e-tests/playwright/lib/src/server/default_config.ts b/e2e-tests/playwright/lib/src/server/default_config.ts index 45bffb2b6b..b88eff4139 100644 --- a/e2e-tests/playwright/lib/src/server/default_config.ts +++ b/e2e-tests/playwright/lib/src/server/default_config.ts @@ -608,6 +608,7 @@ const defaultServerConfig: AdminConfig = { DisableWakeUpReconnectHandler: false, UsersStatusAndProfileFetchingPollIntervalMilliseconds: 3000, YoutubeReferrerPolicy: false, + ExperimentalChannelCategorySorting: false, }, AnalyticsSettings: { MaxUsersForStatistics: 2500, diff --git a/server/channels/app/channel.go b/server/channels/app/channel.go index 534f8f64b2..80c04f87ef 100644 --- a/server/channels/app/channel.go +++ b/server/channels/app/channel.go @@ -181,6 +181,8 @@ func (a *App) CreateChannelWithUser(c request.CTX, channel *model.Channel, userI return nil, err } + a.addChannelToDefaultCategory(c, userID, channel) + var user *model.User if user, err = a.GetUser(userID); err != nil { return nil, err @@ -208,6 +210,10 @@ func (a *App) RenameChannel(c request.CTX, channel *model.Channel, newChannelNam return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_group_messages.app_error", nil, "", http.StatusBadRequest) } + // Clean up the channel name and display name + newChannelName = strings.TrimSpace(newChannelName) + newDisplayName = strings.TrimSpace(newDisplayName) + channel.Name = newChannelName if newDisplayName != "" { channel.DisplayName = newDisplayName @@ -222,6 +228,8 @@ func (a *App) RenameChannel(c request.CTX, channel *model.Channel, newChannelNam } func (a *App) CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) { + a.handleChannelCategoryName(channel) + channel.DisplayName = strings.TrimSpace(channel.DisplayName) sc, nErr := a.Srv().Store().Channel().Save(c, channel, *a.Config().TeamSettings.MaxChannelsPerTeam) if nErr != nil { @@ -864,11 +872,14 @@ func (a *App) PatchChannel(c request.CTX, channel *model.Channel, patch *model.C oldChannelPurpose := channel.Purpose channel.Patch(patch) + a.handleChannelCategoryName(channel) channel, err := a.UpdateChannel(c, channel) if err != nil { return nil, err } + a.addChannelToDefaultCategory(c, userID, channel) + if oldChannelDisplayName != channel.DisplayName { if err = a.PostUpdateChannelDisplayNameMessage(c, userID, channel, oldChannelDisplayName, channel.DisplayName); err != nil { c.Logger().Warn(err.Error()) @@ -1665,6 +1676,8 @@ func (a *App) AddUserToChannel(c request.CTX, user *model.User, channel *model.C return nil, err } + a.addChannelToDefaultCategory(c, user.Id, channel) + // We are sending separate websocket events to the user added and to the channel // This is to get around potential cluster syncing issues where other nodes may not receive the most up to date channel members message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", map[string]bool{user.Id: true}, "") @@ -3878,3 +3891,56 @@ func (a *App) ChannelAccessControlled(c request.CTX, channelID string) (bool, *m return true, nil } + +func (a *App) handleChannelCategoryName(channel *model.Channel) { + if *a.Config().ExperimentalSettings.ExperimentalChannelCategorySorting && strings.Contains(channel.DisplayName, "/") { + parts := strings.Split(channel.DisplayName, "/") + channel.DisplayName = strings.TrimSpace(strings.Join(parts[1:], "/")) + channel.DefaultCategoryName = strings.TrimSpace(parts[0]) + } +} + +func (a *App) addChannelToDefaultCategory(c request.CTX, userID string, channel *model.Channel) { + // Add channel to default category if specified + if channel.DefaultCategoryName != "" && *a.Config().ExperimentalSettings.ExperimentalChannelCategorySorting { + // Get user's categories for this team + categories, err := a.GetSidebarCategoriesForTeamForUser(c, userID, channel.TeamId) + if err != nil { + mlog.Error("Failed to get sidebar categories", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.Err(err)) + return + } + // Find or create the category + var targetCategory *model.SidebarCategoryWithChannels + for _, category := range categories.Categories { + if category.Type == model.SidebarCategoryCustom && strings.EqualFold(category.DisplayName, channel.DefaultCategoryName) { + targetCategory = category + break + } + } + + if targetCategory == nil { + // Create new category if it doesn't exist + targetCategory = &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: userID, + TeamId: channel.TeamId, + Type: model.SidebarCategoryCustom, + DisplayName: channel.DefaultCategoryName, + Sorting: model.SidebarCategorySortDefault, + }, + Channels: []string{channel.Id}, + } + _, err = a.CreateSidebarCategory(c, userID, channel.TeamId, targetCategory) + if err != nil { + mlog.Error("Failed to create default category", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.String("category_name", channel.DefaultCategoryName), mlog.Err(err)) + } + } else { + // Add channel to existing category + targetCategory.Channels = append([]string{channel.Id}, targetCategory.Channels...) + _, err = a.UpdateSidebarCategories(c, userID, channel.TeamId, []*model.SidebarCategoryWithChannels{targetCategory}) + if err != nil { + mlog.Error("Failed to update default category", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.String("category_name", channel.DefaultCategoryName), mlog.Err(err)) + } + } + } +} diff --git a/server/channels/app/channel_test.go b/server/channels/app/channel_test.go index 4f096bbac1..bb2c1fc0c9 100644 --- a/server/channels/app/channel_test.go +++ b/server/channels/app/channel_test.go @@ -3395,3 +3395,139 @@ func TestPatchChannel(t *testing.T) { require.Equal(t, "model.channel.is_valid.banner_info.channel_type.app_error", appErr.Id) }) } + +func TestCreateChannelWithCategorySorting(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // Enable ExperimentalChannelCategorySorting + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ExperimentalSettings.ExperimentalChannelCategorySorting = true + }) + + t.Run("should set category when adding user to channel with category and trim white spaces", func(t *testing.T) { + channel := &model.Channel{ + DisplayName: " Category / Channel Name ", + Name: "name1", + Type: model.ChannelTypeOpen, + TeamId: th.BasicTeam.Id, + } + + channel, appErr := th.App.CreateChannelWithUser(th.Context, channel, th.BasicUser.Id) + require.Nil(t, appErr) + require.Equal(t, "Channel Name", channel.DisplayName) + require.Equal(t, "Category", channel.DefaultCategoryName) + + // Verify channel is in default category + categories, appErr := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser.Id, th.BasicTeam.Id) + require.Nil(t, appErr) + + foundCategory := false + for _, category := range categories.Categories { + if category.DisplayName == "Category" { + foundCategory = true + assert.Contains(t, category.Channels, channel.Id) + break + } + } + assert.True(t, foundCategory, "Category 'Category' not found in sidebar categories") + + // Add user to channel + _, appErr = th.App.AddUserToChannel(th.Context, th.BasicUser2, channel, false) + require.Nil(t, appErr) + + // Verify channel is in default category + categories2, appErr := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser2.Id, th.BasicTeam.Id) + require.Nil(t, appErr) + + foundCategory2 := false + for _, category := range categories2.Categories { + if category.DisplayName == "Category" { + foundCategory2 = true + assert.Contains(t, category.Channels, channel.Id) + break + } + } + assert.True(t, foundCategory2, "Category 'Category' not found in sidebar categories") + }) + + t.Run("should not set category when feature is disabled", func(t *testing.T) { + channel := &model.Channel{ + DisplayName: "Category2/Channel Name", + Name: "name2", + Type: model.ChannelTypeOpen, + TeamId: th.BasicTeam.Id, + } + + channel, appErr := th.App.CreateChannel(th.Context, channel, false) + require.Nil(t, appErr) + require.Equal(t, "Channel Name", channel.DisplayName) + require.Equal(t, "Category2", channel.DefaultCategoryName) + + // Disable ExperimentalChannelCategorySorting + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ExperimentalSettings.ExperimentalChannelCategorySorting = false + }) + + // Add user to channel + _, appErr = th.App.AddUserToChannel(th.Context, th.BasicUser, channel, false) + require.Nil(t, appErr) + + // Verify channel is in default category + categories, appErr := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser.Id, th.BasicTeam.Id) + require.Nil(t, appErr) + + foundCategory := false + for _, category := range categories.Categories { + if category.DisplayName == "Category2" { + foundCategory = true + break + } + } + assert.False(t, foundCategory, "Category 'Category2' not found in sidebar categories") + }) +} + +func TestPatchChannelWithCategorySorting(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // Enable ExperimentalChannelCategorySorting + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ExperimentalSettings.ExperimentalChannelCategorySorting = true + }) + + // Create initial channel + channel := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen) + channel.DisplayName = "Initial Name" + channel, appErr := th.App.UpdateChannel(th.Context, channel) + require.Nil(t, appErr) + + // Add user to channel + _, appErr = th.App.AddUserToChannel(th.Context, th.BasicUser, channel, false) + require.Nil(t, appErr) + + // Patch channel with new display name containing category + patch := &model.ChannelPatch{ + DisplayName: model.NewPointer(" New Category / New Channel Name "), + } + + patchedChannel, appErr := th.App.PatchChannel(th.Context, channel, patch, channel.CreatorId) + require.Nil(t, appErr) + require.Equal(t, "New Channel Name", patchedChannel.DisplayName) + require.Equal(t, "New Category", patchedChannel.DefaultCategoryName) + + // Test that category is not updated when feature is disabled + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ExperimentalSettings.ExperimentalChannelCategorySorting = false + }) + + patch = &model.ChannelPatch{ + DisplayName: model.NewPointer("Disabled Category/Channel Name"), + } + + patchedChannel, appErr = th.App.PatchChannel(th.Context, channel, patch, channel.CreatorId) + require.Nil(t, appErr) + require.Equal(t, "Disabled Category/Channel Name", patchedChannel.DisplayName) + require.Equal(t, "New Category", patchedChannel.DefaultCategoryName) +} diff --git a/server/channels/db/migrations/migrations.list b/server/channels/db/migrations/migrations.list index e7dc749918..bd8cb7e301 100644 --- a/server/channels/db/migrations/migrations.list +++ b/server/channels/db/migrations/migrations.list @@ -271,6 +271,8 @@ channels/db/migrations/mysql/000136_create_attribute_view.down.sql channels/db/migrations/mysql/000136_create_attribute_view.up.sql channels/db/migrations/mysql/000137_update_attribute_view.down.sql channels/db/migrations/mysql/000137_update_attribute_view.up.sql +channels/db/migrations/mysql/000138_add_default_category_name_to_channel.down.sql +channels/db/migrations/mysql/000138_add_default_category_name_to_channel.up.sql channels/db/migrations/postgres/000001_create_teams.down.sql channels/db/migrations/postgres/000001_create_teams.up.sql channels/db/migrations/postgres/000002_create_team_members.down.sql @@ -543,3 +545,5 @@ channels/db/migrations/postgres/000136_create_attribute_view.down.sql channels/db/migrations/postgres/000136_create_attribute_view.up.sql channels/db/migrations/postgres/000137_update_attribute_view.down.sql channels/db/migrations/postgres/000137_update_attribute_view.up.sql +channels/db/migrations/postgres/000138_add_default_category_name_to_channel.down.sql +channels/db/migrations/postgres/000138_add_default_category_name_to_channel.up.sql diff --git a/server/channels/db/migrations/mysql/000138_add_default_category_name_to_channel.down.sql b/server/channels/db/migrations/mysql/000138_add_default_category_name_to_channel.down.sql new file mode 100644 index 0000000000..d45eb09e1e --- /dev/null +++ b/server/channels/db/migrations/mysql/000138_add_default_category_name_to_channel.down.sql @@ -0,0 +1,14 @@ +SET @preparedStatement = (SELECT IF( + EXISTS( + SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS + WHERE table_name = 'Channels' + AND table_schema = DATABASE() + AND column_name = 'DefaultCategoryName' + ), + 'ALTER TABLE Channels DROP COLUMN DefaultCategoryName;', + 'SELECT 1;' +)); + +PREPARE removeColumnIfExists FROM @preparedStatement; +EXECUTE removeColumnIfExists; +DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/channels/db/migrations/mysql/000138_add_default_category_name_to_channel.up.sql b/server/channels/db/migrations/mysql/000138_add_default_category_name_to_channel.up.sql new file mode 100644 index 0000000000..384439d30c --- /dev/null +++ b/server/channels/db/migrations/mysql/000138_add_default_category_name_to_channel.up.sql @@ -0,0 +1,14 @@ +SET @preparedStatement = (SELECT IF( + NOT EXISTS( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Channels' + AND table_schema = DATABASE() + AND column_name = 'DefaultCategoryName' + ), + 'ALTER TABLE Channels ADD COLUMN DefaultCategoryName varchar(64) NOT NULL DEFAULT "";', + 'SELECT 1;' +)); + +PREPARE addColumnIfNotExists FROM @preparedStatement; +EXECUTE addColumnIfNotExists; +DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/channels/db/migrations/postgres/000138_add_default_category_name_to_channel.down.sql b/server/channels/db/migrations/postgres/000138_add_default_category_name_to_channel.down.sql new file mode 100644 index 0000000000..98d64b5462 --- /dev/null +++ b/server/channels/db/migrations/postgres/000138_add_default_category_name_to_channel.down.sql @@ -0,0 +1 @@ +ALTER TABLE channels DROP COLUMN IF EXISTS DefaultCategoryName; \ No newline at end of file diff --git a/server/channels/db/migrations/postgres/000138_add_default_category_name_to_channel.up.sql b/server/channels/db/migrations/postgres/000138_add_default_category_name_to_channel.up.sql new file mode 100644 index 0000000000..907dfb3c0c --- /dev/null +++ b/server/channels/db/migrations/postgres/000138_add_default_category_name_to_channel.up.sql @@ -0,0 +1 @@ +ALTER TABLE channels ADD COLUMN IF NOT EXISTS DefaultCategoryName varchar(64) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/server/channels/store/sqlstore/channel_store.go b/server/channels/store/sqlstore/channel_store.go index 63a3a7952c..e34998a11b 100644 --- a/server/channels/store/sqlstore/channel_store.go +++ b/server/channels/store/sqlstore/channel_store.go @@ -137,6 +137,7 @@ func channelSliceColumns(isSelect bool, prefix ...string) []string { p + "TotalMsgCountRoot", p + "LastRootPostAt", p + "BannerInfo", + p + "DefaultCategoryName", } if isSelect { @@ -172,6 +173,7 @@ func channelToSlice(channel *model.Channel) []any { channel.TotalMsgCountRoot, channel.LastRootPostAt, channel.BannerInfo, + channel.DefaultCategoryName, } } @@ -834,7 +836,8 @@ func (s SqlChannelStore) updateChannelT(transaction *sqlxTxWrapper, channel *mod Shared=:Shared, TotalMsgCountRoot=:TotalMsgCountRoot, LastRootPostAt=:LastRootPostAt, - BannerInfo=:BannerInfo + BannerInfo=:BannerInfo, + DefaultCategoryName=:DefaultCategoryName WHERE Id=:Id`, channel) if err != nil { if IsUniqueConstraintError(err, []string{"Name", "channels_name_teamid_key"}) { diff --git a/server/config/client.go b/server/config/client.go index 01e75e3c93..c67d470743 100644 --- a/server/config/client.go +++ b/server/config/client.go @@ -100,6 +100,7 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["DisableRefetchingOnBrowserFocus"] = strconv.FormatBool(*c.ExperimentalSettings.DisableRefetchingOnBrowserFocus) props["DisableWakeUpReconnectHandler"] = strconv.FormatBool(*c.ExperimentalSettings.DisableWakeUpReconnectHandler) props["UsersStatusAndProfileFetchingPollIntervalMilliseconds"] = strconv.FormatInt(*c.ExperimentalSettings.UsersStatusAndProfileFetchingPollIntervalMilliseconds, 10) + props["ExperimentalChannelCategorySorting"] = strconv.FormatBool(*c.ExperimentalSettings.ExperimentalChannelCategorySorting) // Here we set the new option, but we also send the old FeatureFlag property for backwards compatibility on mobile < 2.27 props["EnableCrossTeamSearch"] = strconv.FormatBool(*c.ServiceSettings.EnableCrossTeamSearch) diff --git a/server/public/model/channel.go b/server/public/model/channel.go index f3cb78702c..1d7309f5ba 100644 --- a/server/public/model/channel.go +++ b/server/public/model/channel.go @@ -77,29 +77,30 @@ func (c ChannelBannerInfo) Value() (driver.Value, error) { } type Channel struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - TeamId string `json:"team_id"` - Type ChannelType `json:"type"` - DisplayName string `json:"display_name"` - Name string `json:"name"` - Header string `json:"header"` - Purpose string `json:"purpose"` - LastPostAt int64 `json:"last_post_at"` - TotalMsgCount int64 `json:"total_msg_count"` - ExtraUpdateAt int64 `json:"extra_update_at"` - CreatorId string `json:"creator_id"` - SchemeId *string `json:"scheme_id"` - Props map[string]any `json:"props"` - GroupConstrained *bool `json:"group_constrained"` - Shared *bool `json:"shared"` - TotalMsgCountRoot int64 `json:"total_msg_count_root"` - PolicyID *string `json:"policy_id"` - LastRootPostAt int64 `json:"last_root_post_at"` - BannerInfo *ChannelBannerInfo `json:"banner_info"` - PolicyEnforced bool `json:"policy_enforced"` + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + TeamId string `json:"team_id"` + Type ChannelType `json:"type"` + DisplayName string `json:"display_name"` + Name string `json:"name"` + Header string `json:"header"` + Purpose string `json:"purpose"` + LastPostAt int64 `json:"last_post_at"` + TotalMsgCount int64 `json:"total_msg_count"` + ExtraUpdateAt int64 `json:"extra_update_at"` + CreatorId string `json:"creator_id"` + SchemeId *string `json:"scheme_id"` + Props map[string]any `json:"props"` + GroupConstrained *bool `json:"group_constrained"` + Shared *bool `json:"shared"` + TotalMsgCountRoot int64 `json:"total_msg_count_root"` + PolicyID *string `json:"policy_id"` + LastRootPostAt int64 `json:"last_root_post_at"` + BannerInfo *ChannelBannerInfo `json:"banner_info"` + PolicyEnforced bool `json:"policy_enforced"` + DefaultCategoryName string `json:"default_category_name"` } func (o *Channel) Auditable() map[string]any { @@ -362,7 +363,7 @@ func (o *Channel) IsOpen() bool { func (o *Channel) Patch(patch *ChannelPatch) { if patch.DisplayName != nil { - o.DisplayName = *patch.DisplayName + o.DisplayName = strings.TrimSpace(*patch.DisplayName) } if patch.Name != nil { diff --git a/server/public/model/config.go b/server/public/model/config.go index 7c622bafdd..2f0387b692 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -1163,6 +1163,7 @@ type ExperimentalSettings struct { DisableWakeUpReconnectHandler *bool `access:"experimental_features"` UsersStatusAndProfileFetchingPollIntervalMilliseconds *int64 `access:"experimental_features"` YoutubeReferrerPolicy *bool `access:"experimental_features"` + ExperimentalChannelCategorySorting *bool `access:"experimental_features"` } func (s *ExperimentalSettings) SetDefaults() { @@ -1213,6 +1214,10 @@ func (s *ExperimentalSettings) SetDefaults() { if s.YoutubeReferrerPolicy == nil { s.YoutubeReferrerPolicy = NewPointer(false) } + + if s.ExperimentalChannelCategorySorting == nil { + s.ExperimentalChannelCategorySorting = NewPointer(false) + } } type AnalyticsSettings struct { diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx index 60980bb22a..2e18f65fc2 100644 --- a/webapp/channels/src/components/admin_console/admin_definition.tsx +++ b/webapp/channels/src/components/admin_console/admin_definition.tsx @@ -6812,6 +6812,13 @@ const AdminDefinition: AdminDefinitionType = { help_text: defineMessage({id: 'admin.experimental.youtubeReferrerPolicy.desc', defaultMessage: 'When true, the referrer policy for embedded YouTube videos will be set to "strict-origin-when-cross-origin" which resolves issues where YouTube video previews display as unavailable, while balancing the need to protect user privacy with some degree of referral data to support web functionalities, like analytics, logging, and third-party integrations. When false, the referrer policy will be set to "no-referrer" which enhances user privacy by not disclosing the source URL, but limits the ability to track user engagement and traffic sources in analytics tools.'}), isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)), }, + { + type: 'bool', + key: 'ExperimentalSettings.ExperimentalChannelCategorySorting', + label: defineMessage({id: 'admin.experimental.channelCategorySorting.title', defaultMessage: 'Channel Category Sorting:'}), + help_text: defineMessage({id: 'admin.experimental.channelCategorySorting.desc', defaultMessage: 'When true, channels will be automatically sorted into categories based on their names using a "/" delimiter.'}), + isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)), + }, ], }, }, diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx index 0f30db0922..42a8bfac3c 100644 --- a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx +++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx @@ -237,7 +237,7 @@ function ChannelSettingsInfoTab({ header: channelHeader.trim(), }; - const {error} = await dispatch(patchChannel(channel.id, updated)); + const {data, error} = await dispatch(patchChannel(channel.id, updated)); if (error) { handleServerError(error as ServerError); return false; @@ -245,10 +245,10 @@ function ChannelSettingsInfoTab({ // After every successful save, update local state to match the saved values // with this, we make sure that the unsavedChanges check will return false after saving - setDisplayName(updated.display_name); - setChannelURL(updated.name); - setChannelPurpose(updated.purpose); - setChannelHeader(updated.header); + setDisplayName(data?.display_name ?? updated.display_name); + setChannelURL(data?.name ?? updated.name); + setChannelPurpose(data?.purpose ?? updated.purpose); + setChannelHeader(data?.header ?? updated.header); return true; }, [channel, displayName, channelUrl, channelPurpose, channelHeader, channelType, setFormError, handleServerError]); diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 39c0adaeb4..6cf2283aef 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -1033,6 +1033,8 @@ "admin.experimental.allowCustomThemes.title": "Allow Custom Themes:", "admin.experimental.allowedEmailDomain.desc": "(Optional) When set, users must have an email ending in this domain to move threads. Multiple domains can be specified by separating them with commas.", "admin.experimental.allowedEmailDomain.title": "Allowed Email Domain", + "admin.experimental.channelCategorySorting.desc": "When true, channels will be automatically sorted into categories based on their names using a \"/\" delimiter.", + "admin.experimental.channelCategorySorting.title": "Channel Category Sorting:", "admin.experimental.clientSideCertCheck.desc": "When **primary**, after the client side certificate is verified, user’s email is retrieved from the certificate and is used to log in without a password. When **secondary**, after the client side certificate is verified, user’s email is retrieved from the certificate and matched against the one supplied by the user. If they match, the user logs in with regular email/password credentials.", "admin.experimental.clientSideCertCheck.options.primary": "primary", "admin.experimental.clientSideCertCheck.options.secondary": "secondary", diff --git a/webapp/platform/types/src/channels.ts b/webapp/platform/types/src/channels.ts index 23635374f3..79e16db550 100644 --- a/webapp/platform/types/src/channels.ts +++ b/webapp/platform/types/src/channels.ts @@ -69,6 +69,7 @@ export type Channel = { policy_id?: string | null; banner_info?: ChannelBanner; policy_enforced?: boolean; + default_category_name?: string; }; export type ServerChannel = Channel & { diff --git a/webapp/platform/types/src/config.ts b/webapp/platform/types/src/config.ts index 34dc704be1..0b1b6852dc 100644 --- a/webapp/platform/types/src/config.ts +++ b/webapp/platform/types/src/config.ts @@ -112,6 +112,7 @@ export type ClientConfig = { EnableUserDeactivation: string; EnableUserTypingMessages: string; EnforceMultifactorAuthentication: string; + ExperimentalChannelCategorySorting: string; ExperimentalClientSideCertCheck: string; ExperimentalClientSideCertEnable: string; ExperimentalEnableAuthenticationTransfer: string; @@ -822,6 +823,7 @@ export type ExperimentalSettings = { DisableWakeUpReconnectHandler: boolean; UsersStatusAndProfileFetchingPollIntervalMilliseconds: number; YoutubeReferrerPolicy: boolean; + ExperimentalChannelCategorySorting: boolean; }; export type AnalyticsSettings = {