Navigation

Room Settings

A Discord-style settings dialog for a room, the one high dialog both settings share, opened from the cog on its row in the room list — including a room the reader is not in, without going there. The panels read the same keyed store slices the room list and the message list do, so those slices follow useRoomStore.scopedRoomId: the route's room, or the room settings is open over. Reading the route's room instead is what made the cog navigate first, and the navigation was visible as a flicker behind the dialog. Its information architecture matches Discord Server Settings: a two-level sidebar of category groups — each a UiCollapsible holding a UiList — whose items are the panels, with the first category headed by the room name itself and Delete (or Leave) kept as a standalone danger row below the categories.

Categories and panels

SettingsCategoryMap owns the grouping; every SettingsType except Delete belongs to exactly one category (enforced by the map's co-located test).

CategoryPanelsGated panels (permission)
(room name)Overview · Roles · Profile · EmojisOverview (ManageRoom), Roles (ManageRoles), Emojis (ManageEmojis)
IntegrationsWebhooksWebhooks (ManageWebhooks)
ModerationWord Filter · Audit Log · Bans · AttachmentsWord Filter + Audit Log + Attachments (ManageRoom), Bans (BanMembers)
User ManagementMembers · InvitesMembers (ManageRoles), Invites (ManageRoom)
(below the categories)Delete · Leavethe owner deletes, every other member leaves

The destructive row is the one whose label follows the reader rather than the panel: Delete for the owner, Leave for everyone else, over the same UiConfirmDialog — the room-name confirmation guard is the owner's only, since leaving is not irreversible (destructive confirmation).

Gating lives in SettingsPermissionMap — a panel with an entry is hidden from members lacking that RoomPermission; a category with no visible panels disappears entirely. Room owners bypass all checks via checkHasPermission. Every panel except Profile carries an entry, and the entry is the same permission the panel's own reads and writes take server-side, which is what a hidden row does not do on its own (RBAC). An ungated row would be a rail entry whose every control rejects. Profile is the exception because it edits the reader's own membership, which every member may do.

MANAGEMENT_PERMISSIONS — the gate on opening the dialog at all, and on the room list's settings button — is the union of that map plus Administrator, derived from it rather than listed beside it. A member who may only manage emoji or bans therefore reaches the rail that manages them, holding one row.

Roles edits roles and their permission bitfields; Members assigns/revokes member roles and searches the room by name, over the readMembers predicate the room list has always accepted — the room's own totals are read once and never re-read against a filtered page, since the roleless group is derived from a count of everybody; Invites lists every active link in the room with its inviter, uses and expiry, pauses invites room-wide, and links into the dialog that creates one, which is where creating stays (invites); Attachments edits the room's upload limits, described in file & media.

How it works

flowchart LR
  Dialog["Settings/Dialog.vue<br/>(singleton, settingsRoomId)"] --> LeftSideBar["LeftSideBar.vue<br/>UiCollapsible per SettingsCategory"]
  Dialog --> Content["Content.vue<br/>Suspense + skeleton"]
  LeftSideBar -- "select SettingsType (closes drawer on mobile)" --> Content
  Content -- "SettingsContentMap[settingsType]" --> Panel["Type/*/Index.vue<br/>lazy async panel"]
  Content -- "mobile hamburger — open v-model" --> LeftSideBar
  LeftSideBar -- "SettingsPermissionMap gate" --> RBAC["useRoleStore.getMyPermissions"]

The dialog mirrors the user settings dialog conventions: panels are lazy defineAsyncComponents rendered in <Suspense :timeout="0"> with the shared MessageModelSettingsSkeleton fallback, and the active panel item in the open category group is highlighted by the generic StyledSlideIndicator rail (items carry data-slide-indicator-key). Unlike the user dialog there is no in-panel section scrollspy — room panels are single-view tools (tables and two-pane editors), so the second nav level selects panels, not scroll sections.

Saving a panel

Every panel that edits the room row saves through useSaveRoom, not through its own executeMutation call. The panel holds its controls as its own refs — deliberately not a clone of the row, so a rejected save leaves what the user entered standing with isDirty still true and the next blur retries it (client data access) — and hands useSaveRoom only the fields it owns. The composable adds the room id, keys the write on it so panels editing different fields of one room queue instead of clobbering each other, and builds the rollback by reading back exactly the keys the save wrote. Snapshotting the whole row instead would revert a field another client changed between the apply and the rejection.

Two panels writing disjoint fields is therefore the normal case rather than a special one, and adding a third is a call with a different field set — never another copy of the optimistic block.

Mobile

The shared MessageModelSettingsLeftSideBar sits beside the panel from the md breakpoint up; below it the sidebar folds away and, while open, covers the panel, opened by the menu button the panel header (MessageModelSettingsHeader) shows only on a narrow screen. Selecting any panel closes it so the content takes the full width — the room dialog threads this open-state through the Dialog (open v-model on the sidebar, open:drawer emit from the header), and the user settings dialog does the same via isDrawerOpen on its dialog store. The two-pane panels that would otherwise sit side by side — Roles and Members (list column + editor column) — are a one-column grid under the sm breakpoint that splits into list and editor columns above it, so neither pane is squeezed.

Key files

FileRole
apps/web/app/models/message/room/SettingsType.tspanel enum (values double as titles)
apps/web/app/models/message/room/SettingsCategory.tssidebar category enum
apps/web/app/services/message/settings/SettingsCategoryMap.tscategory → panels grouping
apps/web/app/services/message/settings/SettingsListItemMap.tspanel icons/colors
apps/web/app/services/message/settings/SettingsContentMap.tspanel → lazy component
apps/web/app/services/message/settings/SettingsPermissionMap.tspanel → required RoomPermission
apps/web/app/composables/message/room/useSaveRoom.tsshared optimistic room-row save + key-scoped rollback
apps/web/app/components/Message/Model/Room/Settings/dialog + sidebar + Type/* panels
apps/web/app/services/room/rbac/constants.tsthe dialog's own gate, derived from the panel map

Details

Command palette

Keyboard shortcuts