Update room list documentation (#34359)
This commit is contained in:
@@ -158,7 +158,7 @@ export default withMermaid({
|
|||||||
{ text: "Iconography", link: "/icons" },
|
{ text: "Iconography", link: "/icons" },
|
||||||
{ text: "Local echo", link: "/local-echo-dev" },
|
{ text: "Local echo", link: "/local-echo-dev" },
|
||||||
{ text: "Media", link: "/media-handling" },
|
{ text: "Media", link: "/media-handling" },
|
||||||
{ text: "Room List Store", link: "/room-list-store" },
|
{ text: "Room List", link: "/room-list" },
|
||||||
{ text: "Scrolling", link: "/scrolling" },
|
{ text: "Scrolling", link: "/scrolling" },
|
||||||
{ text: "Usercontent", link: "/usercontent" },
|
{ text: "Usercontent", link: "/usercontent" },
|
||||||
{ text: "Widget layouts", link: "/widget-layouts" },
|
{ text: "Widget layouts", link: "/widget-layouts" },
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
@@ -1,165 +0,0 @@
|
|||||||
# Room list sorting
|
|
||||||
|
|
||||||
It's so complicated it needs its own README.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Legend:
|
|
||||||
|
|
||||||
- Orange = External event.
|
|
||||||
- Purple = Deterministic flow.
|
|
||||||
- Green = Algorithm definition.
|
|
||||||
- Red = Exit condition/point.
|
|
||||||
- Blue = Process definition.
|
|
||||||
|
|
||||||
## Algorithms involved
|
|
||||||
|
|
||||||
There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting.
|
|
||||||
Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting
|
|
||||||
Algorithm respectively. The list algorithm determines the primary ordering of a given tag whereas the
|
|
||||||
tag sorting defines how rooms within that tag get sorted, at the discretion of the list ordering.
|
|
||||||
|
|
||||||
Behaviour of the overall room list (sticky rooms, etc) are determined by the generically-named Algorithm
|
|
||||||
class. Here is where much of the coordination from the room list store is done to figure out which list
|
|
||||||
algorithm to call, instead of having all the logic in the room list store itself.
|
|
||||||
|
|
||||||
Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm
|
|
||||||
the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm,
|
|
||||||
later described in this document, heavily uses the list ordering behaviour to break the tag into categories.
|
|
||||||
Each category then gets sorted by the appropriate tag sorting algorithm.
|
|
||||||
|
|
||||||
### Tag sorting algorithm: Alphabetical
|
|
||||||
|
|
||||||
When used, rooms in a given tag will be sorted alphabetically, where the alphabet's order is a problem
|
|
||||||
for the browser. All we do is a simple string comparison and expect the browser to return something
|
|
||||||
useful.
|
|
||||||
|
|
||||||
### Tag sorting algorithm: Manual
|
|
||||||
|
|
||||||
Manual sorting makes use of the `order` property present on all tags for a room, per the
|
|
||||||
[Matrix specification](https://matrix.org/docs/spec/client_server/r0.6.0#room-tagging). Smaller values
|
|
||||||
of `order` cause rooms to appear closer to the top of the list.
|
|
||||||
|
|
||||||
### Tag sorting algorithm: Recent
|
|
||||||
|
|
||||||
Rooms get ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm
|
|
||||||
in the room list system which determines whether an event type is capable of bubbling up in the room list.
|
|
||||||
Normally events like room messages, stickers, and room security changes will be considered useful enough
|
|
||||||
to cause a shift in time.
|
|
||||||
|
|
||||||
Note that this is reliant on the event timestamps of the most recent message. Because Matrix is eventually
|
|
||||||
consistent this means that from time to time a room might plummet or skyrocket across the tag due to the
|
|
||||||
timestamp contained within the event (generated server-side by the sender's server).
|
|
||||||
|
|
||||||
### List ordering algorithm: Natural
|
|
||||||
|
|
||||||
This is the easiest of the algorithms to understand because it does essentially nothing. It imposes no
|
|
||||||
behavioural changes over the tag sorting algorithm and is by far the simplest way to order a room list.
|
|
||||||
Historically, it's been the only option in Element and extremely common in most chat applications due to
|
|
||||||
its relative deterministic behaviour.
|
|
||||||
|
|
||||||
### List ordering algorithm: Importance
|
|
||||||
|
|
||||||
On the other end of the spectrum, this is the most complicated algorithm which exists. There's major
|
|
||||||
behavioural changes, and the tag sorting algorithm gets selectively applied depending on circumstances.
|
|
||||||
|
|
||||||
Each tag which is not manually ordered gets split into 4 sections or "categories". Manually ordered tags
|
|
||||||
simply get the manual sorting algorithm applied to them with no further involvement from the importance
|
|
||||||
algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off
|
|
||||||
relative (perceived) importance to the user:
|
|
||||||
|
|
||||||
- **Red**: The room has unread mentions waiting for the user.
|
|
||||||
- **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread
|
|
||||||
messages which cause a push notification or badge count. Typically, this is the default as rooms get
|
|
||||||
set to 'All Messages'.
|
|
||||||
- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
|
|
||||||
a badge/notification count (or 'Mentions Only'/'Muted').
|
|
||||||
- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
|
|
||||||
last read it.
|
|
||||||
|
|
||||||
Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
|
|
||||||
above bold, etc.
|
|
||||||
|
|
||||||
Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm
|
|
||||||
gets applied to each category in a sub-list fashion. This should result in the red rooms (for example)
|
|
||||||
being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but
|
|
||||||
collectively the tag will be sorted into categories with red being at the top.
|
|
||||||
|
|
||||||
## Sticky rooms
|
|
||||||
|
|
||||||
When the user visits a room, that room becomes 'sticky' in the list, regardless of ordering algorithm.
|
|
||||||
From a code perspective, the underlying algorithm is not aware of a sticky room and instead the base class
|
|
||||||
manages which room is sticky. This is to ensure that all algorithms handle it the same.
|
|
||||||
|
|
||||||
The sticky flag is simply to say it will not move higher or lower down the list while it is active. For
|
|
||||||
example, if using the importance algorithm, the room would naturally become idle once viewed and thus
|
|
||||||
would normally fly down the list out of sight. The sticky room concept instead holds it in place, never
|
|
||||||
letting it fly down until the user moves to another room.
|
|
||||||
|
|
||||||
Only one room can be sticky at a time. Room updates around the sticky room will still hold the sticky
|
|
||||||
room in place. The best example of this is the importance algorithm: if the user has 3 red rooms and
|
|
||||||
selects the middle room, they will see exactly one room above their selection at all times. If they
|
|
||||||
receive another notification which causes the room to move into the topmost position, the room that was
|
|
||||||
above the sticky room will move underneath to allow for the new room to take the top slot, maintaining
|
|
||||||
the sticky room's position.
|
|
||||||
|
|
||||||
Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries
|
|
||||||
and thus the user can see a shift in what kinds of rooms move around their selection. An example would
|
|
||||||
be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having
|
|
||||||
the rooms above it read on another device. This would result in 1 red room and 1 other kind of room
|
|
||||||
above the sticky room as it will try to maintain 2 rooms above the sticky room.
|
|
||||||
|
|
||||||
An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement
|
|
||||||
exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain
|
|
||||||
the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed.
|
|
||||||
The N value will never increase while selection remains unchanged: adding a bunch of rooms after having
|
|
||||||
put the sticky room in a position where it's had to decrease N will not increase N.
|
|
||||||
|
|
||||||
## Responsibilities of the store
|
|
||||||
|
|
||||||
The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets
|
|
||||||
an object containing the tags it needs to worry about and the rooms within. The room list component will
|
|
||||||
decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with
|
|
||||||
all kinds of filtering.
|
|
||||||
|
|
||||||
## Filtering
|
|
||||||
|
|
||||||
Filters are provided to the store as condition classes and have two major kinds: Prefilters and Runtime.
|
|
||||||
|
|
||||||
Prefilters flush out rooms which shouldn't appear to the algorithm implementations. Typically this is
|
|
||||||
due to some higher order room list filtering (such as spaces or tags) deliberately exposing a subset of
|
|
||||||
rooms to the user. The algorithm implementations will not see a room being prefiltered out.
|
|
||||||
|
|
||||||
Runtime filters are used for more dynamic filtering, such as the user filtering by room name. These
|
|
||||||
filters are passed along to the algorithm implementations where those implementations decide how and
|
|
||||||
when to apply the filter. In practice, the base `Algorithm` class ends up doing the heavy lifting for
|
|
||||||
optimization reasons.
|
|
||||||
|
|
||||||
The results of runtime filters get cached to avoid needlessly iterating over potentially thousands of
|
|
||||||
rooms, as the old room list store does. When a filter condition changes, it emits an update which (in this
|
|
||||||
case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a
|
|
||||||
minor subset where possible to avoid over-iterating rooms.
|
|
||||||
|
|
||||||
All filter conditions are considered "stable" by the consumers, meaning that the consumer does not
|
|
||||||
expect a change in the condition unless the condition says it has changed. This is intentional to
|
|
||||||
maintain the caching behaviour described above.
|
|
||||||
|
|
||||||
One might ask why we don't just use prefilter conditions for everything, and the answer is one of slight
|
|
||||||
subtlety: in the cases of prefilters we are knowingly exposing the user to a workspace-style UX where
|
|
||||||
room notifications are self-contained within that workspace. Runtime filters tend to not want to affect
|
|
||||||
visible notification counts (as it doesn't want the room header to suddenly be confusing to the user as
|
|
||||||
they type), and occasionally UX like "found 2/12 rooms" is desirable. If prefiltering were used instead,
|
|
||||||
the notification counts would vary while the user was typing and "found 2/12" UX would not be possible.
|
|
||||||
|
|
||||||
## Class breakdowns
|
|
||||||
|
|
||||||
The `RoomListStore` is the major coordinator of various algorithm implementations, which take care
|
|
||||||
of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` class is responsible
|
|
||||||
for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: tags get
|
|
||||||
defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the
|
|
||||||
user). Various list-specific utilities are also included, though they are expected to move somewhere
|
|
||||||
more general when needed. For example, the `membership` utilities could easily be moved elsewhere
|
|
||||||
as needed.
|
|
||||||
|
|
||||||
The various bits throughout the room list store should also have jsdoc of some kind to help describe
|
|
||||||
what they do and how they work.
|
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
# Room list
|
||||||
|
|
||||||
|
The room list is the sorted, filtered list of rooms in the left-hand navigation panel, always
|
||||||
|
scoped to the active space. It is a full [MVVM](MVVM.md) feature split across three layers:
|
||||||
|
|
||||||
|
- **Model** — `RoomListStoreV3`, the store that keeps every room sorted and answers queries for
|
||||||
|
the rooms in the active space. It lives in
|
||||||
|
[`apps/web/src/stores/room-list-v3/`](https://github.com/element-hq/element-web/tree/develop/apps/web/src/stores/room-list-v3/).
|
||||||
|
- **View models** — adapt the store (and other stores) into snapshots for the UI. They live in
|
||||||
|
[`apps/web/src/viewmodels/room-list/`](https://github.com/element-hq/element-web/tree/develop/apps/web/src/viewmodels/room-list/).
|
||||||
|
- **Views** — the presentational React components, owned by `@element-hq/web-shared-components`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
events["Matrix events"] --> store
|
||||||
|
otherStores["Notification / preview / call stores, room events"] --> rlvm
|
||||||
|
otherStores --> itemVM
|
||||||
|
otherStores --> sectionHeaderVM
|
||||||
|
|
||||||
|
subgraph model["Model"]
|
||||||
|
store["RoomListStoreV3(owns the skip list)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph vms["View models"]
|
||||||
|
rlvm["RoomListViewModel"]
|
||||||
|
itemVM["RoomListItemViewModel"]
|
||||||
|
sectionHeaderVM["RoomListSectionHeaderViewModel"]
|
||||||
|
headerVM["RoomListHeaderViewModel"]
|
||||||
|
searchVM["RoomListSearchViewModel"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph views["Views (Shared components)"]
|
||||||
|
rlview["RoomListView"]
|
||||||
|
virtualizedView["VirtualizedRoomListView"]
|
||||||
|
itemView["RoomListItemView"]
|
||||||
|
sectionHeaderView["RoomListSectionHeaderView"]
|
||||||
|
headerView["RoomListHeaderView"]
|
||||||
|
searchView["RoomListSearchView"]
|
||||||
|
end
|
||||||
|
|
||||||
|
rlvm -->|"consume events"| store
|
||||||
|
rlvm -->|"creates"| itemVM
|
||||||
|
rlvm -->|"creates"| sectionHeaderVM
|
||||||
|
rlview -->|"snapshot"| rlvm
|
||||||
|
rlview -->|"renders"| virtualizedView
|
||||||
|
virtualizedView -->|"renders"| itemView
|
||||||
|
virtualizedView -->|"renders"| sectionHeaderView
|
||||||
|
virtualizedView -->|"snapshot"| rlvm
|
||||||
|
itemView -->|"snapshot"| itemVM
|
||||||
|
sectionHeaderView -->|"snapshot"| sectionHeaderVM
|
||||||
|
headerView -->|"snapshot"| headerVM
|
||||||
|
searchView -->|"snapshot"| searchVM
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model — the store
|
||||||
|
|
||||||
|
`RoomListStoreV3` (the "V3" is the third implementation) is a singleton exposed as
|
||||||
|
`RoomListStoreV3.instance`. Unlike the previous implementations, which re-computed lists on
|
||||||
|
demand, V3 keeps every room permanently sorted so retrieval is cheap.
|
||||||
|
|
||||||
|
**The skip list.** The core data structure is a
|
||||||
|
[skip list](https://en.wikipedia.org/wiki/Skip_list): stacked linked lists that keep every room
|
||||||
|
permanently sorted, so a room that changes is simply re-inserted into place and the rest of the
|
||||||
|
list is untouched. Rooms are wrapped in `RoomNode`s that cache whether the room is in the active
|
||||||
|
space and which filters it matches, so reading the list is just an ordered walk that drops the
|
||||||
|
nodes outside the space or not matching the requested filters.
|
||||||
|
|
||||||
|
**Sorters.** A sorter defines the order of the list. There are three: **Alphabetic** (by room
|
||||||
|
name), **Recency** (most recent meaningful activity, with low-priority and muted rooms forced to
|
||||||
|
the bottom), and **Unread** (by unread importance). The user's choice is persisted and can be
|
||||||
|
changed at runtime, which rebuilds the list.
|
||||||
|
|
||||||
|
**Filters.** A filter answers a single yes/no question about a room, and each room's matching
|
||||||
|
filters are precomputed on its node. Filters serve two roles: **capability filters** back the
|
||||||
|
primary filter chips in the UI (Unread, People, Rooms, Favourites, Mentions, Invites, Low
|
||||||
|
Priority), and **section-tag filters** decide which section a room belongs to. Not all of the
|
||||||
|
capability filters are offered as chips at all times: while sectioning is enabled, Favourites and
|
||||||
|
Low Priority are hidden, since those rooms already have their own sections.
|
||||||
|
|
||||||
|
**Sections.** The store groups rooms into named sections (Favourites, Low Priority, Chats, and
|
||||||
|
user-created custom sections) using the section-tag filters described above. Sections are a topic
|
||||||
|
of their own — see [Sections](#sections) below.
|
||||||
|
|
||||||
|
**Spaces and updates.** The list is always scoped to the active space; rather than re-filter on
|
||||||
|
every read, each node caches its active-space membership and the store recomputes that flag when
|
||||||
|
the space changes. The store listens for the Matrix events that affect ordering or membership
|
||||||
|
(receipts, tag changes, account data, push rules, decryption, timeline events, membership) and
|
||||||
|
responds by re-inserting or removing the single affected room. Emissions to consumers are
|
||||||
|
coalesced with `requestAnimationFrame`, so a burst of updates within a frame collapses into one
|
||||||
|
notification.
|
||||||
|
|
||||||
|
**Public surface.** This is the seam the view models bind to:
|
||||||
|
|
||||||
|
- Events: `ListsUpdate` (lists changed), `ListsLoaded` (initial load done), `SectionCreated`,
|
||||||
|
and `RoomTagged` (a locally-initiated tag change, which drives the "chat moved" toast).
|
||||||
|
- `getSortedRoomsInActiveSpace(filterKeys?)` returns the sorted, filtered rooms grouped into
|
||||||
|
sections. Narrower helpers exist too (DM rooms, server-notice rooms, the full sorted list).
|
||||||
|
- Section mutators (`createSection` / `editSection` / `removeSection` / `reorderSection`) and
|
||||||
|
`resort`.
|
||||||
|
|
||||||
|
Note that **sticky-room behaviour lives in the view model, not the store** — the store itself is
|
||||||
|
unaware of a selected room.
|
||||||
|
|
||||||
|
## Sections
|
||||||
|
|
||||||
|
Sections are the named groups the room list displays. Membership is driven entirely by the Matrix
|
||||||
|
`m.tag` account data on each room, so moving a room between sections is just a matter of changing
|
||||||
|
its tags:
|
||||||
|
|
||||||
|
- **Favourites** and **Low Priority** use the default `m.favourite` and `m.lowpriority` tags and
|
||||||
|
are pinned to the top and bottom of the list respectively.
|
||||||
|
- **Chats** is a synthetic catch-all section (tag `chats`) for every room that isn't in any other
|
||||||
|
explicit section.
|
||||||
|
- **Custom sections** are user-created. Each is identified by a generated tag of the form
|
||||||
|
`element.io.section.<uuid>`, and a room is placed in it by tagging the room with that tag.
|
||||||
|
|
||||||
|
The store keeps an ordered list of section tags — Favourites first, Low Priority last, and the
|
||||||
|
custom sections plus Chats reorderable in between (new custom sections are inserted just above
|
||||||
|
Chats by default). Custom sections can be created, renamed, removed, and reordered through
|
||||||
|
dialogs; that logic lives in [`section.ts`](https://github.com/element-hq/element-web/blob/develop/apps/web/src/stores/room-list-v3/section.ts).
|
||||||
|
|
||||||
|
Each custom section also records the space it was created in, which controls the visibility of
|
||||||
|
empty sections: an empty custom section is only shown in the space it belongs to, so sections
|
||||||
|
created in one space don't clutter unrelated spaces. Legacy sections without a stored space (or
|
||||||
|
whose space no longer exists) fall back to the Home meta-space. Note that this visibility rule is
|
||||||
|
applied by `RoomListViewModel` when it builds its snapshot, not by the store.
|
||||||
|
|
||||||
|
### Flat list mode
|
||||||
|
|
||||||
|
When sectioning is turned off, the room list becomes a single flat list instead of a grouped one.
|
||||||
|
This is triggered in two places:
|
||||||
|
|
||||||
|
- If the `RoomList.showSections` setting is disabled, the store skips sectioning altogether and
|
||||||
|
`getSortedRoomsInActiveSpace()` returns a single `chats` section containing every room in the
|
||||||
|
active space.
|
||||||
|
- Even with sectioning enabled, `RoomListViewModel` discards the sections it shouldn't render (the
|
||||||
|
empty ones, and custom sections belonging to another space) and treats what remains as flat
|
||||||
|
(`isFlatList`) if it is nothing at all, or the Chats catch-all on its own — for example when the
|
||||||
|
user has no favourite, low-priority, or custom-tagged rooms.
|
||||||
|
|
||||||
|
In flat mode the view renders a `FlatVirtualizedList` (no section headers); otherwise it renders a
|
||||||
|
`GroupedVirtualizedList` with headers and section drag-and-drop.
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|
Sections are configured entirely through settings:
|
||||||
|
|
||||||
|
- **`RoomList.showSections`** — whether sectioning is enabled at all. When off, the list is flat
|
||||||
|
(see above).
|
||||||
|
- **`RoomList.CustomSectionData`** (account level) — the custom-section definitions, keyed by tag.
|
||||||
|
Each entry stores the tag, the user-chosen name, and the space the section was created in.
|
||||||
|
Malformed entries are dropped when the data is read.
|
||||||
|
- **`RoomList.OrderedCustomSections`** (account level) — the display order of the reorderable
|
||||||
|
sections (the custom sections and Chats). Favourites and Low Priority are not stored here since
|
||||||
|
they are always pinned to the top and bottom.
|
||||||
|
- **`RoomList.SectionExpansionState`** (device level) — the expanded/collapsed state of each
|
||||||
|
section, stored per space and then per section tag. Sections default to expanded when no state
|
||||||
|
has been persisted.
|
||||||
|
|
||||||
|
The account-level settings sync across a user's devices, while the expansion state is
|
||||||
|
device-local so that collapsing a section on one device doesn't affect the others.
|
||||||
|
|
||||||
|
## View models
|
||||||
|
|
||||||
|
The view models in [`apps/web/src/viewmodels/room-list/`](https://github.com/element-hq/element-web/tree/develop/apps/web/src/viewmodels/room-list/)
|
||||||
|
extend `BaseViewModel` and produce immutable snapshots consumed by the views (see [MVVM](MVVM.md)
|
||||||
|
for the base pattern).
|
||||||
|
|
||||||
|
**`RoomListViewModel`** is the root orchestrator and the **only** view model that subscribes to
|
||||||
|
the store's list events and calls `getSortedRoomsInActiveSpace()`. It builds the top-level
|
||||||
|
snapshot — whose sections carry only **room IDs**, not room objects — and owns the sticky-room
|
||||||
|
logic, the active filter, the toasts, section drag-and-drop, and keyboard navigation. It also
|
||||||
|
lazily creates and owns the child view models.
|
||||||
|
|
||||||
|
**`RoomListItemViewModel`** (one per room) and **`RoomListSectionHeaderViewModel`** (one per
|
||||||
|
section) are created on demand by the root view model. Crucially, they do **not** listen to the
|
||||||
|
store's list events; instead they subscribe to fine-grained domain stores directly —
|
||||||
|
notification state, message previews, calls, room events and settings — so a single row can
|
||||||
|
update independently of the rest of the list. The section header view model draws on a narrower
|
||||||
|
set of those, and is _fed_ its set of rooms imperatively by the parent so it can aggregate their
|
||||||
|
notification state.
|
||||||
|
|
||||||
|
**`RoomListHeaderViewModel`** (the header bar: space title, sort menu, create actions) and
|
||||||
|
**`RoomListSearchViewModel`** (the search/dial/explore row) are decoupled siblings of the root
|
||||||
|
view model. They do not share state directly; the header view model and the root view model talk
|
||||||
|
through the global dispatcher, in both directions — collapse-all-sections, for instance, is
|
||||||
|
requested by the header and the resulting state dispatched back to it.
|
||||||
|
|
||||||
|
Permission and create-room helpers used by these view models live in
|
||||||
|
[`utils.ts`](https://github.com/element-hq/element-web/blob/develop/apps/web/src/viewmodels/room-list/utils.ts).
|
||||||
|
|
||||||
|
## Views
|
||||||
|
|
||||||
|
The presentational components are owned by `@element-hq/web-shared-components` (developed in
|
||||||
|
Storybook); `apps/web` supplies the view models, a `renderAvatar` callback and a key handler for
|
||||||
|
landmark navigation, and the shared package owns the rendering. The app-side entry point is
|
||||||
|
[`RoomListPanel`](https://github.com/element-hq/element-web/blob/develop/apps/web/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx), which
|
||||||
|
composes the search row, the header view, and the room list itself.
|
||||||
|
|
||||||
|
The room list proper is `RoomListView`, which renders the filter chips and any toast above a body
|
||||||
|
that is a loading skeleton, an empty state, or — in the usual case — `VirtualizedRoomListView`
|
||||||
|
(built on [`react-virtuoso`](https://virtuoso.dev/)). Virtualization drives the lazy lifecycle of
|
||||||
|
the child view models: as rows scroll into view the list calls back through
|
||||||
|
`getRoomItemViewModel(id)` and `getSectionHeaderViewModel(tag)` to obtain the view model for each
|
||||||
|
rendered room or header, and reports its visible range so off-screen item view models can be
|
||||||
|
disposed. Because each row binds to its own view model, it re-renders on its own data without
|
||||||
|
re-rendering the whole list.
|
||||||
|
|
||||||
|
The list renders either as a flat list or a grouped list with section headers, depending on the
|
||||||
|
`isFlatList` flag in the snapshot (see [Flat list mode](#flat-list-mode)); in the grouped case,
|
||||||
|
drag-and-drop is wired back to the root view model, which reorders sections, moves rooms between
|
||||||
|
them, and collapses the sections for the duration of a section drag.
|
||||||
Reference in New Issue
Block a user