fix(room list): stop reading custom section settings on every room item update (#34102)

RoomListItemViewModel rebuilt the "Move to section" submenu list via
getCustomSectionData() inside every snapshot rebuild - i.e. on every
notification, name, tag or call update of every live room item - even
though the available sections only change when the custom section
settings change. Each call rebuilds the record from settings and does
an O(spaces) existence check per section.

Cache the available sections (tag + name) in the view model and
rebuild them only from the settings watchers; per-room snapshots now
only derive the cheap isSelected flag. Also watch
RoomList.CustomSectionData so section renames propagate to the
submenu (previously only the order setting was watched).
This commit is contained in:
Florian Duros
2026-07-02 13:50:01 +00:00
committed by GitHub
parent 557107d7b9
commit 1d32b838ac
@@ -42,6 +42,11 @@ import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
import { getCustomSectionData, isDefaultSectionTag } from "../../stores/room-list-v3/section";
import { _t } from "../../languageHandler";
/**
* View section type without `isSelected` field
*/
type Sections = Array<Omit<Section, "isSelected">>;
interface RoomItemProps {
room: Room;
client: MatrixClient;
@@ -61,14 +66,29 @@ export class RoomListItemViewModel
* Track the current call for this room to manager listeners
*/
private currentCall: Call | null = null;
/**
* The sections available in the "Move to section" submenu (tag + display name), identical
* for every update of this item. Computing it reads the custom section settings, so it is
* built once and rebuilt only when those settings change (see {@link onCustomSectionsChange})
* rather than on every snapshot rebuild; only the per-room isSelected flag is derived per
* snapshot (see {@link buildSections}).
*/
private availableSections: Sections;
public constructor(props: RoomItemProps) {
// Get notification state first so we can generate a complete initial snapshot
const notifState = RoomNotificationStateStore.instance.getRoomState(props.room);
const initialItem = RoomListItemViewModel.generateItemSync(props.room, props.client, notifState);
const availableSections = RoomListItemViewModel.computeAvailableSections();
const initialItem = RoomListItemViewModel.generateItemSync(
props.room,
props.client,
notifState,
availableSections,
);
super(props, initialItem);
this.notifState = notifState;
this.availableSections = availableSections;
// Subscribe to notification state changes for this room
this.disposables.trackListener(this.notifState, NotificationStateEvents.Update, this.onNotificationChanged);
@@ -99,11 +119,16 @@ export class RoomListItemViewModel
this.disposables.trackListener(props.room, RoomEvent.Name, this.onRoomChanged);
this.disposables.trackListener(props.room, RoomEvent.Tags, this.onRoomChanged);
// Rebuild the available sections when their order changes or when one is created/renamed/removed
const orderSectionsRef = SettingsStore.watchSetting("RoomList.OrderedCustomSections", null, () =>
this.onOrderedCustomSectionsChange(),
this.onCustomSectionsChange(),
);
const sectionDataRef = SettingsStore.watchSetting("RoomList.CustomSectionData", null, () =>
this.onCustomSectionsChange(),
);
this.disposables.track(() => {
SettingsStore.unwatchSetting(orderSectionsRef);
SettingsStore.unwatchSetting(sectionDataRef);
});
// Load message preview asynchronously (sync data is already complete)
@@ -187,7 +212,12 @@ export class RoomListItemViewModel
* Preserves the message preview which is managed separately.
*/
private updateItem(): void {
const newItem = RoomListItemViewModel.generateItemSync(this.props.room, this.props.client, this.notifState);
const newItem = RoomListItemViewModel.generateItemSync(
this.props.room,
this.props.client,
this.notifState,
this.availableSections,
);
this.snapshot.merge({
...newItem,
notification: keepIfSame(this.snapshot.current.notification, newItem.notification),
@@ -233,6 +263,7 @@ export class RoomListItemViewModel
room: Room,
client: MatrixClient,
notifState: RoomNotificationState,
availableSections: Sections,
): RoomListItemViewSnapshot {
// Get room tags for menu state
const roomTags = room.tags;
@@ -289,7 +320,7 @@ export class RoomListItemViewModel
call?.callType === CallType.Voice ? "voice" : call?.callType === CallType.Video ? "video" : undefined;
// Build sections list for the "Move to section" submenu
const sections: Section[] = RoomListItemViewModel.buildSections(roomTags);
const sections: Section[] = RoomListItemViewModel.buildSections(roomTags, availableSections);
return {
id: room.roomId,
@@ -420,17 +451,20 @@ export class RoomListItemViewModel
}
};
private onOrderedCustomSectionsChange = (): void => {
// Rebuild sections list to reflect new order
const sections = RoomListItemViewModel.buildSections(this.props.room.tags);
private onCustomSectionsChange = (): void => {
// Rebuild the available sections and the sections list to reflect the new settings
this.availableSections = RoomListItemViewModel.computeAvailableSections();
const sections = RoomListItemViewModel.buildSections(this.props.room.tags, this.availableSections);
this.snapshot.merge({ sections: keepIfSame(this.snapshot.current.sections, sections) });
};
/**
* Build the list of available sections for the "Move to section" submenu.
* Compute the sections available in the "Move to section" submenu.
* Order follows the canonical section order from RoomListStoreV3.
* Reads the custom section settings, so callers should cache the result and recompute only
* when those settings change.
*/
private static buildSections(roomTags: Room["tags"]): Section[] {
private static computeAvailableSections(): Sections {
const customSectionData = getCustomSectionData();
return (
@@ -441,11 +475,22 @@ export class RoomListItemViewModel
.map((tag) => ({
tag,
name: RoomListItemViewModel.getSectionName(tag, customSectionData),
isSelected: Boolean(roomTags[tag]),
}))
);
}
/**
* Build the list of sections for the "Move to section" submenu by deriving the per-room
* selected state from the room's tags.
*/
private static buildSections(roomTags: Room["tags"], availableSections: Sections): Section[] {
return availableSections.map(({ tag, name }) => ({
tag,
name,
isSelected: Boolean(roomTags[tag]),
}));
}
/**
* Get the display name for a section based on its tag.
*/