Remove legacy room list (#34040)

* refactor(room-list): migrate SpaceStore off the legacy room list store

SpaceStore.setActiveRoomInSpace iterated the legacy RoomListStore's
`orderedLists` in `TAG_ORDER`; switch it to the space-aware
RoomListStoreV3.getSortedRoomsInActiveSpace() accessor. This drops the
last non-UI dependency on the legacy store and on `TAG_ORDER` (exported
from LegacyRoomList, deleted next).

* feat(room-list)!: remove the legacy room list UI

Delete the old sublist-based room list and its components now that the
new RoomListPanel is the default. Removed: LegacyRoomList,
LegacyRoomListHeader, RoomSublist, ExtraTile, RoomTile (+ Subtitle/
CallSummary), RoomBreadcrumbs and RoomSearch, plus their styles and
tests.

LeftPanel collapses to the RoomListPanel-only path. The shared
`contextMenuBelow` helper is relocated into RoomResultContextMenus (its
only remaining consumer).

* feat(room-list)!: remove the legacy RoomListStore

The legacy sublist-based room list UI is gone, so the old
`stores/room-list` store (Algorithm, sorters, filters, layout store,
space
watcher) has no remaining consumers. Delete the directory and its tests.

MatrixChat.forgetRoom no longer calls the legacy `manualRoomUpdate`; the
new room list store removes the room on the `AfterForgetRoom` dispatch
that still fires. Drop the `mxRoomListStore`/`mxRoomListLayoutStore`
globals and the now-dead test imports.

* feat(room-list)!: remove the feature_new_room_list labs flag

The new room list is now the only room list, so remove the
feature_new_room_list labs flag and make its enabled behaviour
unconditional everywhere it was gated:

- LoggedInView: always use the resizable layout and
NEW_ROOM_LIST_MIN_WIDTH;
  drop the collapsible/minimized legacy path.
- SpaceStore: People and Favourites are dropped from metaSpaceOrder (per
the
  long-standing TODO on the removed accessor).
- MessagePreviewStore: stop appending thread replies to previews.
- Settings, SidebarUserSettingsTab, PreferencesUserSettingsTab,
  QuickSettingsButton, SpacePanel, LandmarkNavigation: drop the flag
reads and
  legacy branches.

Update the tests that toggled the flag; the People/Favourites meta space
tests covered behaviour that the flag (default on) already disabled.

* feat(room-list)!: remove the dead legacy left-panel resizer

LoggedInView still built the old `Resizer`/`CollapseDistributor` over an
`lp-resizer` ResizeHandle and persisted `mx_lhs_size`. That handle is no
longer rendered (the resizable layout is now driven by
LeftResizablePanelView + ResizerViewModel, which persists its own state
via RoomList.panelSize/RoomList.isPanelCollapsed), so the old resizer
was
inert dead code left over from the legacy room list.

Remove createResizer/loadResizer/loadResizerPreferences, the
_resizeContainer/resizeHandler refs, the ResizeHandle render, the
mx_lhs_size handling and NEW_ROOM_LIST_MIN_WIDTH, plus the unit tests
that
exercised the mocked resizer.

* feat(room-list)!: update i18n files

* refactor(room-list): remove the now-unused collapseLhs state

`collapseLhs` is write-only since the left panel no longer collapses: it
was last read by LoggedInView's `shouldUseMinimizedUI`, removed with the
feature_new_room_list flag. Drop it from MatrixChat's IState (and its
assignments), collapsing the hide/show_left_panel handlers to just the
`notifyLeftHandleResized()` call they still need, and from
LoggedInView's
IProps and the test props.

* fix(room-list): instantiate message previewers lazily

Removing the unused SettingsStore import from MessagePreviewStore (when
the
feature_new_room_list flag was dropped) changed module load order and
exposed a latent circular dependency: ReactionEventPreview imports
MessagePreviewStore, which eagerly did `new ReactionEventPreview()` at
module-eval — so importing ReactionEventPreview first (as its unit test
does) hit "ReactionEventPreview is not a constructor".

Construct the previewers lazily on first use (cached) instead of at
module
load, so nothing dereferences a mid-evaluation module. Fixes
ReactionEventPreview-test.

* test(room-list): remove `feature_new_room_list` labs flag in e2e tests

* chore: remove remaining `newRoomList` flag

* chore: cleanup theme files

* fix: restore the re-resizable TouchEvent polyfill

* chore: remove usage of breadcrumbs settings in BreadcrumbStore

* Revert "fix(room-list): instantiate message previewers lazily"

This reverts commit 4e6eedfff0449c68a96c0470a4eb425b5aec5512.

* chore: remove unused function in BreadCrumbStore

* test: remove unused fuction of BreadcrumStore in tests

* test: add tests for RoomResultContextMenu
This commit is contained in:
Florian Duros
2026-07-02 09:07:37 +00:00
committed by GitHub
parent 522ec3aa57
commit b68ecdb860
103 changed files with 91 additions and 11086 deletions
@@ -1,43 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 Mikhail Aheichyk
Copyright 2023 Nordeck IT + Consulting GmbH.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, type RenderResult, screen } from "jest-matrix-react";
import { mocked } from "jest-mock";
import LeftPanel from "../../../../src/components/structures/LeftPanel";
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents";
import { UIComponent } from "../../../../src/settings/UIFeature";
jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
}));
describe("LeftPanel", () => {
function renderComponent(): RenderResult {
return render(<LeftPanel isMinimized={false} resizeNotifier={new ResizeNotifier()} />);
}
it("does not show filter container when disabled by UIComponent customisations", () => {
mocked(shouldShowComponent).mockReturnValue(false);
renderComponent();
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.FilterContainer);
expect(screen.queryByRole("button", { name: /search/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Explore rooms" })).not.toBeInTheDocument();
});
it("renders filter container when enabled by UIComponent customisations", () => {
mocked(shouldShowComponent).mockReturnValue(true);
renderComponent();
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.FilterContainer);
expect(screen.getByRole("button", { name: /search/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Explore rooms" })).toBeInTheDocument();
});
});
@@ -41,27 +41,6 @@ import { SETTINGS } from "../../../../src/settings/Settings";
import ToastStore from "../../../../src/stores/ToastStore";
import { ModuleApi } from "../../../../src/modules/Api";
// Create a mock resizer instance that can be shared across tests
const mockResizerInstance = {
attach: jest.fn(),
detach: jest.fn(),
forHandleWithId: jest.fn().mockReturnValue({ resize: jest.fn() }),
setClassNames: jest.fn(),
};
// Mock the Resizer module
jest.mock("../../../../src/resizer", () => {
const originalModule = jest.requireActual("../../../../src/resizer");
return {
...originalModule,
Resizer: jest.fn().mockImplementation((container, distributorBuilder, collapseConfig) => {
// Store the callbacks globally for test access
(global as any).__resizeCallbacks = collapseConfig;
return mockResizerInstance;
}),
};
});
describe("<LoggedInView />", () => {
const userId = "@alice:domain.org";
const mockClient = getMockClientWithEventEmitter({
@@ -86,7 +65,6 @@ describe("<LoggedInView />", () => {
matrixClient: mockClient,
onRegistered: jest.fn(),
resizeNotifier: new ResizeNotifier(),
collapseLhs: false,
hideToSRUsers: false,
config: {
brand: "Test",
@@ -535,100 +513,18 @@ describe("<LoggedInView />", () => {
});
});
describe("resizer preferences", () => {
let mockResize: jest.Mock;
let mockForHandleWithId: jest.Mock;
beforeEach(() => {
// Clear localStorage before each test
window.localStorage.clear();
mockResize = jest.fn();
mockForHandleWithId = jest.fn().mockReturnValue({ resize: mockResize });
// Update the shared mock instance for this test
mockResizerInstance.forHandleWithId = mockForHandleWithId;
// Clear any global callback state
delete (global as any).__resizeCallbacks;
});
it("should call resize with default size when localStorage contains NaN value", () => {
// Set invalid value in localStorage that will result in NaN
window.localStorage.setItem("mx_lhs_size", "not-a-number");
getComponent();
// Verify that when lhsSize is NaN, it defaults to 350 and calls resize
expect(mockForHandleWithId).toHaveBeenCalledWith("lp-resizer");
expect(mockResize).toHaveBeenCalledWith(350);
});
it("should use existing size when localStorage contains valid value", () => {
// Set valid value in localStorage
window.localStorage.setItem("mx_lhs_size", "400");
getComponent();
// Verify the resize method was called with the stored size (400)
expect(mockResize).toHaveBeenCalledWith(400);
});
it("should enforce minimum width for new room list when stored size is zero", async () => {
// Enable new room list feature
await SettingsStore.setValue("feature_new_room_list", null, SettingLevel.DEVICE, true);
// 0 represents the collapsed state for the old room list, which could have been set before the new room list was enabled
window.localStorage.setItem("mx_lhs_size", "0");
getComponent();
// Verify the resize method was called with the default size (350) when stored size is below minimum
expect(mockResize).toHaveBeenCalledWith(350);
});
it("should not set localStorage to 0 when resizing lp-resizer to minimum width for new room list", async () => {
// Enable new room list feature and mock SettingsStore
await SettingsStore.setValue("feature_new_room_list", null, SettingLevel.DEVICE, true);
const minimumWidth = 224; // NEW_ROOM_LIST_MIN_WIDTH
// Render the component
getComponent();
// Get the callbacks that were captured during resizer creation
const callbacks = (global as any).__resizeCallbacks;
// Create a mock DOM node for isItemCollapsed to check
const domNode = {
classList: {
contains: jest.fn().mockReturnValue(true), // Simulate the error where mx_LeftPanel_minimized is present
},
} as any;
callbacks.onResized(minimumWidth);
const isCollapsed = callbacks.isItemCollapsed(domNode);
callbacks.onCollapsed(isCollapsed); // Not collapsed for new room list
callbacks.onResizeStop();
// Verify localStorage was set to the minimum width (224), not 0
expect(window.localStorage.getItem("mx_lhs_size")).toBe("224");
});
});
describe("module-rendered fullscreen view (e.g. multiroom)", () => {
// A page_type for which a module registers a custom full-screen renderer.
const modulePageType = "io.element.test_fullscreen";
beforeEach(async () => {
await SettingsStore.setValue("feature_new_room_list", null, SettingLevel.DEVICE, true);
beforeEach(() => {
ModuleApi.instance.navigation.registerLocationRenderer(modulePageType, () => (
<div data-testid="module-content" />
));
});
afterEach(async () => {
afterEach(() => {
ModuleApi.instance.navigation.locationRenderers.delete(modulePageType);
await SettingsStore.setValue("feature_new_room_list", null, SettingLevel.DEVICE, false);
});
it("renders the resizable separator for a normal room view with the new room list", () => {
@@ -650,34 +546,4 @@ describe("<LoggedInView />", () => {
expect(container.querySelector(".mx_SpacePanel")).toBeInTheDocument();
});
});
describe("create a new resizer when page_type changes", () => {
afterEach(() => {
jest.clearAllMocks();
});
it("should call loadResizer when page_type changes", () => {
const component = getComponent({ page_type: "room" });
// Re-render with different page_type
component.rerender(<LoggedInView {...defaultProps} page_type="home" />);
// Verify that detach was called (from loadResizer)
expect(mockResizerInstance.detach).toHaveBeenCalledTimes(1);
// Verify that attach was called (from loadResizer)
// 1 (when page_type = "room") + 1 (when page_type = "home")
expect(mockResizerInstance.attach).toHaveBeenCalledTimes(2);
});
it("should not call loadResizer when page_type remains the same", () => {
const component = getComponent({ page_type: "room" });
// Re-render with same page_type but different other props
component.rerender(<LoggedInView {...defaultProps} page_type="room" currentRoomId="!different:room.id" />);
// Verify that resizer methods were not called
expect(mockResizerInstance.detach).not.toHaveBeenCalled();
expect(mockResizerInstance.attach).toHaveBeenCalledTimes(1);
});
});
});
@@ -68,7 +68,6 @@ import Modal from "../../../../src/Modal.tsx";
import { SetupEncryptionStore } from "../../../../src/stores/SetupEncryptionStore.ts";
import { ShareFormat } from "../../../../src/dispatcher/payloads/SharePayload.ts";
import { clearStorage } from "../../../../src/Lifecycle";
import RoomListStore from "../../../../src/stores/room-list/RoomListStore.ts";
import UserSettingsDialog from "../../../../src/components/views/dialogs/UserSettingsDialog.tsx";
import { SDKContextClass } from "../../../../src/contexts/SDKContextClass";
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc.ts";
@@ -854,9 +853,6 @@ describe("<MatrixChat />", () => {
await clearAllModals();
await getComponentAndWaitForReady();
// Mock out the old room list store
jest.spyOn(RoomListStore.instance, "manualRoomUpdate").mockImplementation(async () => {});
// Register a mock function to the dispatcher
const fn = jest.fn();
defaultDispatcher.register(fn);