MVVM - Introduce some helpers for snapshot management (#30398)
* Introduce snapshot class to track snapshot updates This avoids the hassle of having to manually call emit. * Better viewmodel ergonomics - Rename `SubscriptionViewModel` to `BaseViewModel`. I feel this is appropriate since that class does more than just manage subscriptions. - `getSnapshot` is no longer an abstract method. It's simply a method that returns the current snapshot state. This ensures that getSnapshot result is cached by default which is required by `useSyncExternalStore`. - `props` are a property of the base vm class so that actual VMs don't have to keep creating this property. * Update `TextualEventViewModel` * Fix test * Rename `TextualEvent` to `TextualEventView` * Fix snapshot object not being merged * Rename directory to `EventTileView` * Fix broken snapshot * Add test for snapshot class
This commit is contained in:
@@ -5,17 +5,24 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ViewModel } from "../shared-components/ViewModel";
|
||||
import { type ViewModel } from "../../shared-components/ViewModel";
|
||||
import { Snapshot } from "./Snapshot";
|
||||
import { ViewModelSubscriptions } from "./ViewModelSubscriptions";
|
||||
|
||||
export abstract class SubscriptionViewModel<T> implements ViewModel<T> {
|
||||
export abstract class BaseViewModel<T, P> implements ViewModel<T> {
|
||||
protected subs: ViewModelSubscriptions;
|
||||
protected snapshot: Snapshot<T>;
|
||||
protected props: P;
|
||||
|
||||
protected constructor() {
|
||||
protected constructor(props: P, initialSnapshot: T) {
|
||||
this.props = props;
|
||||
this.subs = new ViewModelSubscriptions(
|
||||
this.addDownstreamSubscriptionWrapper,
|
||||
this.removeDownstreamSubscriptionWrapper,
|
||||
);
|
||||
this.snapshot = new Snapshot(initialSnapshot, () => {
|
||||
this.subs.emit();
|
||||
});
|
||||
}
|
||||
|
||||
public subscribe = (listener: () => void): (() => void) => {
|
||||
@@ -52,5 +59,7 @@ export abstract class SubscriptionViewModel<T> implements ViewModel<T> {
|
||||
/**
|
||||
* Returns the current snapshot of the view model.
|
||||
*/
|
||||
public abstract getSnapshot: () => T;
|
||||
public getSnapshot = (): T => {
|
||||
return this.snapshot.current;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -10,29 +10,25 @@ import { MatrixEventEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { type EventTileTypeProps } from "../../events/EventTileFactory";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import { textForEvent } from "../../TextForEvent";
|
||||
import { type TextualEventViewSnapshot } from "../../shared-components/event-tiles/TextualEvent/TextualEvent";
|
||||
import { SubscriptionViewModel } from "../SubscriptionViewModel";
|
||||
import { type TextualEventViewSnapshot } from "../../shared-components/event-tiles/TextualEventView/TextualEventView";
|
||||
import { BaseViewModel } from "../base/BaseViewModel";
|
||||
|
||||
export class TextualEventViewModel extends SubscriptionViewModel<TextualEventViewSnapshot> {
|
||||
public constructor(private eventTileProps: EventTileTypeProps) {
|
||||
super();
|
||||
export class TextualEventViewModel extends BaseViewModel<TextualEventViewSnapshot, EventTileTypeProps> {
|
||||
public constructor(props: EventTileTypeProps) {
|
||||
super(props, { content: "" });
|
||||
this.setTextFromEvent();
|
||||
}
|
||||
|
||||
private setTextFromEvent = (): void => {
|
||||
const content = textForEvent(this.props.mxEvent, MatrixClientPeg.safeGet(), true, this.props.showHiddenEvents);
|
||||
this.snapshot.set({ content });
|
||||
};
|
||||
|
||||
protected addDownstreamSubscription = (): void => {
|
||||
this.eventTileProps.mxEvent.on(MatrixEventEvent.SentinelUpdated, this.subs.emit);
|
||||
this.props.mxEvent.on(MatrixEventEvent.SentinelUpdated, this.setTextFromEvent);
|
||||
};
|
||||
|
||||
protected removeDownstreamSubscription = (): void => {
|
||||
this.eventTileProps.mxEvent.off(MatrixEventEvent.SentinelUpdated, this.subs.emit);
|
||||
};
|
||||
|
||||
public getSnapshot = (): TextualEventViewSnapshot => {
|
||||
const text = textForEvent(
|
||||
this.eventTileProps.mxEvent,
|
||||
MatrixClientPeg.safeGet(),
|
||||
true,
|
||||
this.eventTileProps.showHiddenEvents,
|
||||
);
|
||||
return text;
|
||||
this.props.mxEvent.off(MatrixEventEvent.SentinelUpdated, this.setTextFromEvent);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user