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:
R Midhun Suresh
2025-08-06 12:29:32 +00:00
committed by GitHub
parent 15f1291cbc
commit ee37734cfc
12 changed files with 130 additions and 34 deletions
@@ -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;
};
}
+43
View File
@@ -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);
};
}