mirror of
https://github.com/vector-im/element-web.git
synced 2026-03-04 04:52:04 +01:00
Merge branch 'develop' into hs/url-preview-vm
This commit is contained in:
commit
ea6f20c30c
20
apps/web/src/utils/keepIfSame.ts
Normal file
20
apps/web/src/utils/keepIfSame.ts
Normal 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;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
22
apps/web/test/unit-tests/utils/keepIfSame-test.ts
Normal file
22
apps/web/test/unit-tests/utils/keepIfSame-test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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", () => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user