From 53e038b09b4e2099caea8fecc5069f4fe6ef3036 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 8 Oct 2026 10:57:03 +0200 Subject: [PATCH] layoutShallowEquals -> shallowEquals --- src/state/CallViewModel/CallViewModel.ts | 8 +-- src/state/layout-types.test.ts | 74 ------------------------ src/state/layout-types.ts | 29 ---------- src/utils/object.test.ts | 40 +++++++++++++ src/utils/object.ts | 38 ++++++++++++ 5 files changed, 82 insertions(+), 107 deletions(-) delete mode 100644 src/state/layout-types.test.ts create mode 100644 src/utils/object.test.ts create mode 100644 src/utils/object.ts diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index ae6a54697..083666001 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -82,6 +82,7 @@ import { type ReactionOption, } from "../../reactions"; import { shallowEquals as shallowArrayEquals } from "../../utils/array"; +import { shallowEquals as shallowObjectEquals } from "../../utils/object"; import { type MediaDevices } from "../MediaDevices"; import { constant, type Behavior } from "../Behavior"; import { E2eeType } from "../../e2ee/e2eeType"; @@ -91,7 +92,6 @@ import { HeaderStyle, type UrlParams } from "../../UrlParams"; import { type ProcessorState } from "../../livekit/TrackProcessorContext"; import { type HostBridge, nullHostBridge } from "../../HostBridge"; import { - layoutShallowEquals, type Alignment, type Layout, type LayoutMedia, @@ -1028,7 +1028,7 @@ export function createCallViewModel$( [ringingMedia$, screenShares$, spotlightSpeaker$, localUserMediaForPip$], (ringing, screenShares, speaker, localPip) => computeSpotlight({ ringing, screenShares, speaker, localPip }), - ).pipe(distinctUntilChanged(layoutShallowEquals)), + ).pipe(distinctUntilChanged(shallowObjectEquals)), ); const spotlight$ = scope.behavior( @@ -1185,7 +1185,7 @@ export function createCallViewModel$( desktop: platform === "desktop", }), ), - distinctUntilChanged(layoutShallowEquals), + distinctUntilChanged(shallowObjectEquals), ), ); @@ -1486,7 +1486,7 @@ export function createCallViewModel$( layoutInternals$.pipe( map(({ layout }) => layout), // Drop redundant layout updates before they would hit React. - distinctUntilChanged(layoutShallowEquals), + distinctUntilChanged(shallowObjectEquals), ), ); diff --git a/src/state/layout-types.test.ts b/src/state/layout-types.test.ts deleted file mode 100644 index 468c15bb5..000000000 --- a/src/state/layout-types.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright 2026 Element Creations Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { test, expect } from "vitest"; -import { - type Alignment, - layoutShallowEquals, - type Layout, -} from "./layout-types"; -import { - type SpotlightTileViewModel, - type GridTileViewModel, -} from "./TileViewModel"; -import { BehaviorSubject } from "rxjs"; - -const spotlightTile = {} as unknown as SpotlightTileViewModel; -const gridTile = {} as unknown as GridTileViewModel; -const pipAlignment$ = new BehaviorSubject({ - inline: "end", - block: "end", -}); - -const spotlightExpanded: Layout = { - type: "spotlight-expanded", - spotlight: spotlightTile, - pipAlignment$, -}; - -const spotlightPortrait: Layout = { - type: "spotlight-portrait", - spotlight: spotlightTile, - grid: [gridTile], - setVisibleTiles: () => {}, -}; - -test("layoutShallowEquals considers a layout to be equal to its shallow clone", () => - expect(layoutShallowEquals(spotlightExpanded, { ...spotlightExpanded })).toBe( - true, - )); - -test("layoutShallowEquals detects a missing key", () => { - expect( - layoutShallowEquals(spotlightExpanded, { - ...spotlightExpanded, - pip: gridTile, - }), - ).toBe(false); - expect( - layoutShallowEquals( - { ...spotlightExpanded, pip: gridTile }, - spotlightExpanded, - ), - ).toBe(false); -}); - -test("layoutShallowEquals considers grid arrays with equal contents to be equal", () => - expect( - layoutShallowEquals(spotlightPortrait, { - ...spotlightPortrait, - grid: [...spotlightPortrait.grid], - }), - ).toBe(true)); - -test("layoutShallowEquals detects grid arrays with different contents", () => - expect( - layoutShallowEquals(spotlightPortrait, { - ...spotlightPortrait, - grid: [...spotlightPortrait.grid, gridTile], - }), - ).toBe(false)); diff --git a/src/state/layout-types.ts b/src/state/layout-types.ts index fd9b36a69..3d80b361e 100644 --- a/src/state/layout-types.ts +++ b/src/state/layout-types.ts @@ -16,7 +16,6 @@ import { type SpotlightTileViewModel, } from "./TileViewModel.ts"; import { type Behavior } from "./Behavior.ts"; -import { shallowEquals as arrayShallowEquals } from "../utils/array.ts"; /** * The general shape of the space the call is drawn in. Called a window because @@ -148,31 +147,3 @@ export type Layout = | OneOnOneDesktopLayout | OneOnOneMobileLayout | PipLayout; - -/** - * Tests whether the top-level properties and array elements of `a` are equal - * to those of `b`. Useful for deduping redundant layout and layout media - * updates. - */ -export function layoutShallowEquals(a: T, b: T): boolean { - // If a and b have the same number of keys and every key in a is also in b, - // then they have the same keys. - const aKeys = Object.keys(a); - const bKeys = Object.keys(b); - if (aKeys.length !== bKeys.length) return false; - - for (const key of aKeys) { - if (!(key in b)) return false; - - // Now check that they have the same values. - const aValue = (a as any)[key]; - const bValue = (b as any)[key]; - if (Array.isArray(aValue) && Array.isArray(bValue)) { - // Special case for arrays so we can detect when the grid tiles arrays are - // essentially the same. - if (!arrayShallowEquals(aValue, bValue)) return false; - } else if (aValue !== bValue) return false; - } - - return true; -} diff --git a/src/utils/object.test.ts b/src/utils/object.test.ts new file mode 100644 index 000000000..6b55ac7f3 --- /dev/null +++ b/src/utils/object.test.ts @@ -0,0 +1,40 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { test, expect } from "vitest"; +import { shallowEquals } from "./object"; + +interface Thing { + type: string; + items: object[]; + value: number; + extra?: object; +} + +const item = {}; +const withArray: Thing = { type: "a", items: [item], value: 1 }; + +test("shallowEquals considers an object to be equal to its shallow clone", () => + expect(shallowEquals(withArray, { ...withArray })).toBe(true)); + +test("shallowEquals detects a missing key", () => { + expect(shallowEquals(withArray, { ...withArray, extra: item })).toBe(false); + expect(shallowEquals({ ...withArray, extra: item }, withArray)).toBe(false); +}); + +test("shallowEquals detects a differing value", () => + expect(shallowEquals(withArray, { ...withArray, value: 2 })).toBe(false)); + +test("shallowEquals considers arrays with equal contents to be equal", () => + expect( + shallowEquals(withArray, { ...withArray, items: [...withArray.items] }), + ).toBe(true)); + +test("shallowEquals detects arrays with different contents", () => + expect( + shallowEquals(withArray, { ...withArray, items: [...withArray.items, {}] }), + ).toBe(false)); diff --git a/src/utils/object.ts b/src/utils/object.ts new file mode 100644 index 000000000..7fc7b5b1d --- /dev/null +++ b/src/utils/object.ts @@ -0,0 +1,38 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { shallowEquals as arrayShallowEquals } from "./array.ts"; + +/** + * Determine whether two objects are equal by shallow comparison of their + * top-level properties. Array-valued properties are themselves compared + * shallowly, so two objects holding distinct arrays with the same elements are + * considered equal. + */ +export function shallowEquals( + a: Readonly, + b: Readonly, +): boolean { + // If a and b have the same number of keys and every key in a is also in b, + // then they have the same keys. + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + if (aKeys.length !== bKeys.length) return false; + + for (const key of aKeys) { + if (!(key in b)) return false; + + // Now check that they have the same values. + const aValue = (a as any)[key]; + const bValue = (b as any)[key]; + if (Array.isArray(aValue) && Array.isArray(bValue)) { + if (!arrayShallowEquals(aValue, bValue)) return false; + } else if (aValue !== bValue) return false; + } + + return true; +}