Instantiate message previewers lazily (#34065)

* fix(room-list): instantiate message previewers lazily

Removing the unused SettingsStore import from MessagePreviewStore (when
the
feature_new_room_list flag is dropped) changed module load order and
exposed a latent circular dependency: ReactionEventPreview imports
MessagePreviewStore, which eagerly did `new ReactionEventPreview()` at
module-eval — so importing ReactionEventPreview first (as its unit test
does) hit "ReactionEventPreview is not a constructor".

Construct the previewers lazily on first use (cached) instead of at
module
load, so nothing dereferences a mid-evaluation module. Fixes
ReactionEventPreview-test.

* refactor: pass MessagePreviewStore to ReactEventPreview
This commit is contained in:
Florian Duros
2026-07-01 10:14:41 +00:00
committed by GitHub
parent e836eb04e2
commit 27d97d3661
4 changed files with 104 additions and 47 deletions
@@ -37,47 +37,6 @@ import SettingsStore from "../../settings/SettingsStore";
// the change happened.
const ROOM_PREVIEW_CHANGED = "room_preview_changed";
const PREVIEWS: Record<
string,
{
isState: boolean;
previewer: Preview;
}
> = {
"m.room.message": {
isState: false,
previewer: new MessageEventPreview(),
},
"m.call.invite": {
isState: false,
previewer: new LegacyCallInviteEventPreview(),
},
"m.call.answer": {
isState: false,
previewer: new LegacyCallAnswerEventPreview(),
},
"m.call.hangup": {
isState: false,
previewer: new LegacyCallHangupEvent(),
},
"m.sticker": {
isState: false,
previewer: new StickerEventPreview(),
},
"m.reaction": {
isState: false,
previewer: new ReactionEventPreview(),
},
[M_POLL_START.name]: {
isState: false,
previewer: new PollStartEventPreview(),
},
[M_POLL_START.altName]: {
isState: false,
previewer: new PollStartEventPreview(),
},
};
// The maximum number of events we're willing to look back on to get a preview.
const MAX_EVENTS_BACKWARDS = 50;
@@ -139,8 +98,23 @@ export class MessagePreviewStore extends AsyncStoreWithClient<EmptyObject> {
// null indicates the preview is empty / irrelevant
private previews = new Map<string, Map<TagID | TAG_ANY, MessagePreview | null>>();
// Previewers keyed by event type
private readonly previewers: Record<string, { isState: boolean; previewer: Preview }>;
private constructor() {
super(defaultDispatcher, {});
this.previewers = {
"m.room.message": { isState: false, previewer: new MessageEventPreview() },
"m.call.invite": { isState: false, previewer: new LegacyCallInviteEventPreview() },
"m.call.answer": { isState: false, previewer: new LegacyCallAnswerEventPreview() },
"m.call.hangup": { isState: false, previewer: new LegacyCallHangupEvent() },
"m.sticker": { isState: false, previewer: new StickerEventPreview() },
// ReactionEventPreview needs the message preview store, so the previewers are constructed here (passing `this`) rather than at
// module scope, where the store singleton would not exist yet (circular dependency).
"m.reaction": { isState: false, previewer: new ReactionEventPreview(this) },
[M_POLL_START.name]: { isState: false, previewer: new PollStartEventPreview() },
[M_POLL_START.altName]: { isState: false, previewer: new PollStartEventPreview() },
};
}
public static get instance(): MessagePreviewStore {
@@ -172,7 +146,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<EmptyObject> {
}
public generatePreviewForEvent(event: MatrixEvent): string {
const previewDef = PREVIEWS[event.getType()];
const previewDef = this.previewers[event.getType()];
return previewDef?.previewer.getTextFor(event, undefined, true) ?? "";
}
@@ -217,7 +191,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<EmptyObject> {
await this.matrixClient?.decryptEventIfNeeded(event);
const shouldHide = shouldHideEvent(event);
if (shouldHide) continue;
const previewDef = PREVIEWS[event.getType()];
const previewDef = this.previewers[event.getType()];
if (!previewDef) continue;
if (previewDef.isState && isNullOrUndefined(event.getStateKey())) continue;
@@ -13,9 +13,11 @@ import { type TagID } from "../../room-list-v3/skip-list/tag";
import { getSenderName, isSelf } from "./utils";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MessagePreviewStore } from "../MessagePreviewStore";
import type { MessagePreviewStore } from "../MessagePreviewStore";
export class ReactionEventPreview implements Preview {
public constructor(private readonly messagePreviewStore: MessagePreviewStore) {}
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null {
const roomId = event.getRoomId();
if (!roomId) return null; // not a room event
@@ -31,7 +33,7 @@ export class ReactionEventPreview implements Preview {
const relatedEvent = relation.event_id ? room?.findEventById(relation.event_id) : null;
if (!relatedEvent) return null;
const message = MessagePreviewStore.instance.generatePreviewForEvent(relatedEvent);
const message = this.messagePreviewStore.generatePreviewForEvent(relatedEvent);
if (isSelf(event)) {
return _t("event_preview|m.reaction|you", {
reaction,