Files
upstream-rocketchat/docs/features/custom-sidebar-categories.md
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

11 KiB

Custom Sidebar Categories

Overview

Custom categories let each user group their sidebar rooms into personal, named collections that appear alongside the system groups (Favorites, Teams, Channels, Direct messages, …). A user can create, rename, delete and reorder categories; move rooms in and out of them via context menus; and toggle whether each category keeps showing unread rooms while collapsed.

Categories are per-user and client-only in meaning — they are a presentation grouping of the user's own subscriptions. They carry no access, security or membership semantics: moving a room into a category never changes who can see the room or the room itself. Assignment is exclusive — a room lives in exactly one place at a time (one custom category, or Favorites, or its system group).

Requires the experimental-enterprise-features license module.

Data Model

Category record — ISidebarCategory

// packages/core-typings/src/IUser.ts
export interface ISidebarCategory {
    _id: string;               // Random.id() for custom; system group key for defaults
    name: string;              // display name (trimmed; max 30 chars)
    default?: boolean;         // true for system-group entries managed by the platform
    showUnreads?: boolean;     // when collapsed, keep listing unread rooms (default: false)
    keepUnreadsOnTop?: boolean; // when expanded, sort unread rooms to the top (default: false)
}

Room assignment

Room-to-category assignment is stored on the subscription, not on the category:

// Subscription.category: string | null

Set via POST /experimental/rooms.setCategory. The subscription field is the source of truth for which category a room belongs to; the category record only carries display metadata and ordering.

Persistence

sidebarCategories preference (unified group ordering + category metadata)

The preference sidebarCategories: ISidebarCategory[] is the single source of truth for both the ordered list of all visible groups and the metadata of custom categories. It contains entries for custom categories and for any system group whose metadata has been touched (show-unreads, keep-unreads-on-top, or position).

Invariant enforced on every write: all five dynamic groups (Incoming_Calls, Incoming_Livechats, Open_Livechats, On_Hold_Chats, Unread) are always stored first, in the order prescribed by sidebarSectionsOrder. Custom and static system groups follow. This is enforced by withDynamicFirst in useCategoryList.ts, called by every write hook.

All mutations go through usePersistCategoriesMutation, which calls:

POST /v1/users.setPreferences  { data: { sidebarCategories: ISidebarCategory[] } }

Room assignment — POST /experimental/rooms.setCategory

POST /experimental/rooms.setCategory
{ roomIds: string[], category: string | null }
  • Requires experimental-enterprise-features license.
  • Rejects system group keys (SIDEBAR_SYSTEM_GROUP_KEYS) as the target category.
  • Verifies the target category exists in the user's sidebarCategories preference before writing.
  • Writes to subscription.category and fires change notifications.
  • category: null unassigns the room, returning it to its system group.

Group Ordering

sidebarSectionsOrder preference

An admin-configurable ordered list of system group keys that defines the default group order when sidebarCategories has no explicit entry for a key. Falls back to SIDEBAR_SYSTEM_GROUP_KEYS.

mergeWithSectionsOrder(explicitIds, sectionsOrder) — READ side

Used in useCategoryList when building the visible category list. Inserts any key missing from explicitIds before its first successor that is already present, maintaining the sectionsOrder relative ordering for system groups.

withDynamicFirst(ids, sectionsOrder) — WRITE side

Called by every write hook before persisting sidebarCategories. Guarantees:

  1. All five dynamic group keys appear first (in sectionsOrder canonical order), regardless of whether they are currently visible.
  2. Static ids (custom + system) follow, merged with sectionsOrder via mergeWithSectionsOrder.

This makes moving logic simple: swapping two adjacent static groups is always a valid operation — dynamic groups can never appear below a static group in the stored list, so a DOWN move can never cross one.

SIDEBAR_DYNAMIC_GROUP_KEYS

export const SIDEBAR_DYNAMIC_GROUP_KEYS: readonly string[] = [
    'Incoming_Calls', 'Incoming_Livechats', 'Open_Livechats', 'On_Hold_Chats', 'Unread',
];

Groups that are hidden from the sidebar when empty. They are excluded from user reordering (canMoveGroup returns false for them) and are always stored first by withDynamicFirst.

Behaviors / Flows

Create — useCreateCustomCategory

  1. Generates _id with Random.id().
  2. Inserts the new entry at the first position after the last dynamic group entry in the stored list (using findLastIndex), so it appears first among static groups.
  3. Optionally assigns rooms via useSetCategory in the same call (Create and move flow).

Move room — useMoveRoomCategory

Accepts a target (category _id, 'favorites' sentinel, or undefined to remove).

  • 'favorites': calls POST /v1/rooms.favorite and removes the category assignment.
  • Custom category _id: calls POST /experimental/rooms.setCategory.
  • undefined / same as current: removes assignment (unfavorites or sets category to null).
  • Dispatches a toast unless silent: true or the room has no name.

Move category position — useMoveCategoryPosition

Takes currentKeys (visible group keys in render order), a key to move, and a direction. Performs an adjacent swap in currentKeys, then calls withDynamicFirst on the result before persisting — so dynamic groups are always re-sorted to the front regardless of what the swap produced.

Toggle show-unreads / keep-unreads-on-top — useToggleUnreads

upsertGroupEntry(id, patch) — updates the matching ISidebarCategory entry in sidebarCategories (or creates a new one if the entry doesn't exist yet). Always calls withDynamicFirst before persisting.

Delete — useDeleteCategory

Removes the category entry from sidebarCategories and calls POST /experimental/rooms.setCategory with category: null for all rooms that were in it — returning them to their system group.

Name validation — useValidateCategoryName

Returns an i18n error string or undefined. Checks in order:

  1. Empty / whitespace → Please_enter_a_category_name
  2. Longer than MAX_CATEGORY_NAME_LENGTH (30) → Category_name_is_too_long__max__maxLength__characters
  3. Matches a translated system group name (case-insensitive) → Category_name_conflicts_with_system_group
  4. Duplicate among existing custom categories (case-insensitive, excluding excludeId) → A_category_with_this_name_already_exists

Favorites interaction

FAVORITES_TARGET = 'favorites' is a sentinel for the Favorites system group. Moving a room to Favorites calls POST /v1/rooms.favorite and strips the category assignment. Moving a room from Favorites into a category unfavorites it. Assignment is exclusive.

Visibility in the room list

filterGroupVisibility (in useCategoryList.ts) applies when building the room list:

  • Dynamic groups — only shown when non-empty.
  • All other groups (custom + static system) — always shown when EE is active, even if empty.

This means custom categories are always present in the visible group list with EE, so no category entry in sidebarCategories can be silently dropped during a move.

Sidebar UI

Category collapser kebab — CategoryMenu.tsx

Custom category:

  • Order: Move up / Move down / New channel
  • Manage: Rename / Delete / New category
  • When collapsed: Show unreads toggle
  • When expanded: Keep unreads on top toggle

System group:

  • Order: Move up / Move down (dynamic groups have both disabled)
  • When collapsed: Show unreads toggle
  • When expanded: Keep unreads on top toggle

Room context menu — CategoryRoomMenu.tsx

For rooms inside a custom category, adds a "Remove from {category}" item.

Room kebab — useCategoryMenuItems.ts

Builds the "Move to" submenu: Favorites + each custom category with a checkmark on the current one. Appended with a "New category" item and "Remove from {category}" when applicable.

Room-header grouping menu — RoomHeaderCategoryMenu.tsx

Placed before the room title in RoomHeader. Icon: star-filled (favorited) / folder (custom category) / star (default). Same item list as the room kebab submenu.

Modals — categories/

File Purpose
CreateCategoryModal.tsx Create or "create and move" — single name field, max 30 chars.
ManageCategoryModal.tsx Rename with duplicate/empty validation.
DeleteCategoryModal.tsx Confirmation + "rooms will return to default groups" warning.
hooks/useCategoryModals.tsx Single entry point: openCreate(room?) / openManage(category) / openDelete(category).

Navbar create menu

useCreateNewMenu.ts adds a "Category" item (icon: folder) in the navbar + menu.

REST Contract

Method Endpoint Purpose
POST /v1/users.setPreferences Persist sidebarCategories (ordering + metadata)
POST /experimental/rooms.setCategory Assign / unassign rooms to a category
POST /v1/rooms.favorite Favorite / unfavorite (used by the Favorites target)

Key Files

Layer File
Types packages/core-typings/src/IUser.ts (ISidebarCategory, SIDEBAR_SYSTEM_GROUP_KEYS)
Category list + ordering apps/meteor/client/sidebar/hooks/useCategoryList.ts
Room list apps/meteor/client/sidebar/hooks/useRoomList.ts
Persist mutation apps/meteor/client/sidebar/categories/hooks/usePersistCategoriesMutation.ts
Create category apps/meteor/client/sidebar/categories/hooks/useCreateCustomCategory.ts
Move room apps/meteor/client/sidebar/categories/hooks/useMoveRoomCategory.ts
Move category position apps/meteor/client/sidebar/categories/hooks/useMoveCategoryPosition.ts
Toggle unreads apps/meteor/client/sidebar/categories/hooks/useToggleUnreads.ts
Name validation apps/meteor/client/sidebar/categories/hooks/useValidateCategoryName.ts
Custom categories list apps/meteor/client/sidebar/categories/hooks/useCustomCategories.ts
Set category (client) apps/meteor/client/sidebar/categories/hooks/useSetCategory.ts
Server endpoint apps/meteor/server/api/experimental/rooms.setCategory.ts
Category UI apps/meteor/client/sidebar/categories/
Room kebab apps/meteor/client/sidebar/RoomMenu.tsx
Navbar create menu apps/meteor/client/navbar/NavBarPagesGroup/hooks/useCreateNewMenu.ts
Room-header grouping apps/meteor/client/views/room/Header/icons/RoomGroupingMenu.tsx
i18n packages/i18n/src/locales/en.i18n.json

Tests

Kind File
API (preference persistence + validation) apps/meteor/tests/end-to-end/api/sidebar-custom-categories.ts
E2E (Playwright) apps/meteor/tests/e2e/sidebar-custom-categories.spec.ts
E2E page object apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts (custom-category helpers)