Move view model code to shared components package (#31024)

* Remove vm related code from element-web/src

* Add and export view model code from package

* Update imports

* Rewrite vm tests using vitest

* Add github action to run vm tests

* Fix lint errors

* Mvoe tests over to jest

* Try fixing code coverage

* Second attempt at fixing code coverage
This commit is contained in:
R Midhun Suresh
2025-10-15 19:19:12 +05:30
committed by GitHub
parent ac96ab0d46
commit 2598e4ea22
22 changed files with 29 additions and 17 deletions

View File

@@ -17,7 +17,7 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { percentageOf } from "../../../packages/shared-components/src/utils/numbers";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { BaseViewModel } from "../base/BaseViewModel";
import { BaseViewModel } from "../../../packages/shared-components/src/viewmodel";
/**
* The number of seconds to skip when the user presses the left or right arrow keys.

View File

@@ -1,51 +0,0 @@
/*
Copyright 2025 New Vector 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 { type ViewModel } from "../../../packages/shared-components/src/ViewModel";
import { Disposables } from "./Disposables";
import { Snapshot } from "./Snapshot";
import { ViewModelSubscriptions } from "./ViewModelSubscriptions";
export abstract class BaseViewModel<T, P> implements ViewModel<T> {
protected subs: ViewModelSubscriptions;
protected snapshot: Snapshot<T>;
protected props: P;
protected disposables = new Disposables();
protected constructor(props: P, initialSnapshot: T) {
this.props = props;
this.subs = new ViewModelSubscriptions();
this.snapshot = new Snapshot(initialSnapshot, () => {
this.subs.emit();
});
}
public subscribe = (listener: () => void): (() => void) => {
return this.subs.add(listener);
};
/**
* Returns the current snapshot of the view model.
*/
public getSnapshot = (): T => {
return this.snapshot.current;
};
/**
* Relinquish any resources held by this view-model.
*/
public dispose(): void {
this.disposables.dispose();
}
/**
* Whether this view-model has been disposed.
*/
public get isDisposed(): boolean {
return this.disposables.isDisposed;
}
}

View File

@@ -1,70 +0,0 @@
/*
Copyright 2025 New Vector 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 type { EventEmitter } from "events";
/**
* Something that needs to be eventually disposed. This can be:
* - A function that does the disposing
* - An object containing a dispose method which does the disposing
*/
export type DisposableItem = { dispose: () => void } | (() => void);
/**
* This class provides a way for the view-model to track any resource
* that it needs to eventually relinquish.
*/
export class Disposables {
private readonly disposables: DisposableItem[] = [];
private _isDisposed: boolean = false;
/**
* Relinquish all tracked disposable values
*/
public dispose(): void {
if (this.isDisposed) return;
this._isDisposed = true;
for (const disposable of this.disposables) {
if (typeof disposable === "function") {
disposable();
} else {
disposable.dispose();
}
}
}
/**
* Track a value that needs to be eventually relinquished
*/
public track<T extends DisposableItem>(disposable: T): T {
this.throwIfDisposed();
this.disposables.push(disposable);
return disposable;
}
/**
* Add an event listener that will be removed on dispose
*/
public trackListener(emitter: EventEmitter, event: string, callback: (...args: unknown[]) => void): void {
this.throwIfDisposed();
emitter.on(event, callback);
this.track(() => {
emitter.off(event, callback);
});
}
private throwIfDisposed(): void {
if (this.isDisposed) throw new Error("Disposable is already disposed");
}
/**
* Whether this disposable has been disposed
*/
public get isDisposed(): boolean {
return this._isDisposed;
}
}

View File

@@ -1,43 +0,0 @@
/*
Copyright 2025 New Vector 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.
*/
/**
* This is the output of the viewmodel that the view consumes.
* Updating snapshot through this object will make react re-render
* components.
*/
export class Snapshot<T> {
public constructor(
private snapshot: T,
private emit: () => void,
) {}
/**
* Replace current snapshot with a new snapshot value.
* @param snapshot New snapshot value
*/
public set(snapshot: T): void {
this.snapshot = snapshot;
this.emit();
}
/**
* Update a part of the current snapshot by merging into the existing snapshot.
* @param snapshot A subset of the snapshot to merge into the current snapshot.
*/
public merge(snapshot: Partial<T>): void {
this.snapshot = { ...this.snapshot, ...snapshot };
this.emit();
}
/**
* The current value of the snapshot.
*/
public get current(): T {
return this.snapshot;
}
}

View File

@@ -1,34 +0,0 @@
/*
Copyright 2025 New Vector 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.
*/
/**
* Utility class for view models to manage subscriptions to their updates
*/
export class ViewModelSubscriptions {
private listeners = new Set<() => void>();
/**
* Subscribe to changes in the view model.
* @param listener Will be called whenever the snapshot changes.
* @returns A function to unsubscribe from the view model updates.
*/
public add = (listener: () => void): (() => void) => {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
};
/**
* Emit an update to all subscribed listeners.
*/
public emit = (): void => {
for (const listener of this.listeners) {
listener();
}
};
}

View File

@@ -11,7 +11,7 @@ import { type EventTileTypeProps } from "../../events/EventTileFactory";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { textForEvent } from "../../TextForEvent";
import { type TextualEventViewSnapshot } from "../../../packages/shared-components/src/event-tiles/TextualEventView/TextualEventView";
import { BaseViewModel } from "../base/BaseViewModel";
import { BaseViewModel } from "../../../packages/shared-components/src/viewmodel";
export class TextualEventViewModel extends BaseViewModel<TextualEventViewSnapshot, EventTileTypeProps> {
public constructor(props: EventTileTypeProps) {