Merge branch 'develop' into hs/url-preview-vm

This commit is contained in:
Will Hunt 2026-03-03 10:05:24 +00:00 committed by GitHub
commit ea6f20c30c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 71 additions and 31 deletions

View File

@ -0,0 +1,20 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* 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 { isEqual } from "lodash";
/**
* Returns the current value if it is deeply equal to the next value, otherwise returns the next value.
* This is useful to prevent unnecessary re-renders in React components when the value has not changed.
* @param current The current value
* @param next The next value
* @returns The current value if it is deeply equal to the next value, otherwise the next value
*/
export function keepIfSame<T>(current: T, next: T): T {
if (isEqual(current, next)) return current;
return next;
}

View File

@ -25,6 +25,7 @@ import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotif
import { RoomListItemViewModel } from "./RoomListItemViewModel";
import { SdkContextClass } from "../../contexts/SDKContext";
import { hasCreateRoomRights } from "./utils";
import { keepIfSame } from "../../utils/keepIfSame";
interface RoomListViewViewModelProps {
client: MatrixClient;
@ -133,9 +134,6 @@ export class RoomListViewViewModel
// Update roomsMap immediately before clearing VMs
this.updateRoomsMap(this.roomsResult);
// Clear view models since room list changed
this.clearViewModels();
this.updateRoomListData();
};
@ -291,11 +289,13 @@ export class RoomListViewViewModel
const newSpaceId = this.roomsResult.spaceId;
// Clear view models since room list structure changed
this.clearViewModels();
// Detect space change
if (oldSpaceId !== newSpaceId) {
// Clear view models when the space changes
// We only want to do this on space changes, not on regular list updates, to preserve view models when possible
// The view models are disposed when scrolling out of view (handled by updateVisibleRooms)
this.clearViewModels();
// Space changed - get the last selected room for the new space to prevent flicker
const lastSelectedRoom = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpaceId);
@ -408,13 +408,16 @@ export class RoomListViewViewModel
// Build the complete state atomically to ensure consistency
// roomIds and roomListState must always be in sync
const roomIds = this.roomIds;
// Update filter keys - only update if they have actually changed to prevent unnecessary re-renders of the room list
const previousFilterKeys = this.snapshot.current.roomListState.filterKeys;
const newFilterKeys = this.roomsResult.filterKeys?.map((k) => String(k));
const roomListState: RoomListViewState = {
activeRoomIndex,
spaceId: this.roomsResult.spaceId,
filterKeys: this.roomsResult.filterKeys?.map((k) => String(k)),
filterKeys: keepIfSame(previousFilterKeys, newFilterKeys),
};
const filterIds = [...filterKeyToIdMap.values()];
const activeFilterId = this.activeFilter !== undefined ? filterKeyToIdMap.get(this.activeFilter) : undefined;
const isRoomListEmpty = roomIds.length === 0;
const isLoadingRooms = RoomListStoreV3.instance.isLoadingRooms;
@ -423,7 +426,6 @@ export class RoomListViewViewModel
this.snapshot.merge({
isLoadingRooms,
isRoomListEmpty,
filterIds,
activeFilterId,
roomListState,
roomIds,

View File

@ -0,0 +1,22 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* 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 { keepIfSame } from "../../../src/utils/keepIfSame";
describe("keepIfSame", () => {
it("returns the next value if the current and next values are not deeply equal", () => {
const current = { a: 1 };
const next = { a: 2 };
expect(keepIfSame(current, next)).toBe(next);
});
it("returns the current value if the current and next values are deeply equal", () => {
const current = { a: 1 };
const next = { a: 1 };
expect(keepIfSame(current, next)).toBe(current);
});
});

View File

@ -124,6 +124,20 @@ describe("RoomListViewViewModel", () => {
expect(viewModel.getSnapshot().isLoadingRooms).toBe(false);
});
// This test ensures that the room list item vms are preserved when the room list is changing
it("should keep existing view model when ListsUpdate event fires", () => {
viewModel = new RoomListViewViewModel({ client: matrixClient });
// Create view model for room1
const room1VM = viewModel.getRoomItemViewModel("!room1:server");
expect(room1VM).toBeDefined();
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
// View model should be still valid
expect(room1VM.isDisposed).toBe(false);
});
});
describe("Space switching", () => {
@ -295,24 +309,6 @@ describe("RoomListViewViewModel", () => {
expect(viewModel.getSnapshot().activeFilterId).toBeUndefined();
expect(viewModel.getSnapshot().roomIds).toEqual(["!room1:server", "!room2:server", "!room3:server"]);
});
it("should clear view models when filter changes", () => {
viewModel = new RoomListViewViewModel({ client: matrixClient });
// Get view models
const vm1 = viewModel.getRoomItemViewModel("!room1:server");
const disposeSpy = jest.spyOn(vm1, "dispose");
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
rooms: [room2],
filterKeys: [FilterKey.UnreadFilter],
});
viewModel.onToggleFilter("unread");
expect(disposeSpy).toHaveBeenCalled();
});
});
describe("Room item view models", () => {

View File

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, useId, useState } from "react";
import React, { type JSX, memo, useId, useState } from "react";
import { ChatFilter, IconButton } from "@vector-im/compound-web";
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
@ -53,11 +53,11 @@ export interface RoomListPrimaryFiltersProps {
* The primary filters component for the room list.
* Displays a collapsible list of filters with expand/collapse functionality.
*/
export const RoomListPrimaryFilters: React.FC<RoomListPrimaryFiltersProps> = ({
export const RoomListPrimaryFilters = memo(function RoomListPrimaryFilters({
filterIds,
activeFilterId,
onToggleFilter,
}): JSX.Element | null => {
}: RoomListPrimaryFiltersProps): JSX.Element | null {
const id = useId();
const [isExpanded, setIsExpanded] = useState(false);
@ -113,4 +113,4 @@ export const RoomListPrimaryFilters: React.FC<RoomListPrimaryFiltersProps> = ({
</Flex>
</Flex>
);
};
});