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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user