snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,243 @@
# 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`
```ts
// 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:
```ts
// 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:
```http
POST /v1/users.setPreferences { data: { sidebarCategories: ISidebarCategory[] } }
```
### Room assignment — `POST /experimental/rooms.setCategory`
```http
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`
```ts
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) |