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:
@@ -37,47 +37,6 @@ import SettingsStore from "../../settings/SettingsStore";
|
|||||||
// the change happened.
|
// the change happened.
|
||||||
const ROOM_PREVIEW_CHANGED = "room_preview_changed";
|
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.
|
// The maximum number of events we're willing to look back on to get a preview.
|
||||||
const MAX_EVENTS_BACKWARDS = 50;
|
const MAX_EVENTS_BACKWARDS = 50;
|
||||||
|
|
||||||
@@ -139,8 +98,23 @@ export class MessagePreviewStore extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
// null indicates the preview is empty / irrelevant
|
// null indicates the preview is empty / irrelevant
|
||||||
private previews = new Map<string, Map<TagID | TAG_ANY, MessagePreview | null>>();
|
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() {
|
private constructor() {
|
||||||
super(defaultDispatcher, {});
|
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 {
|
public static get instance(): MessagePreviewStore {
|
||||||
@@ -172,7 +146,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generatePreviewForEvent(event: MatrixEvent): string {
|
public generatePreviewForEvent(event: MatrixEvent): string {
|
||||||
const previewDef = PREVIEWS[event.getType()];
|
const previewDef = this.previewers[event.getType()];
|
||||||
return previewDef?.previewer.getTextFor(event, undefined, true) ?? "";
|
return previewDef?.previewer.getTextFor(event, undefined, true) ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +191,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
await this.matrixClient?.decryptEventIfNeeded(event);
|
await this.matrixClient?.decryptEventIfNeeded(event);
|
||||||
const shouldHide = shouldHideEvent(event);
|
const shouldHide = shouldHideEvent(event);
|
||||||
if (shouldHide) continue;
|
if (shouldHide) continue;
|
||||||
const previewDef = PREVIEWS[event.getType()];
|
const previewDef = this.previewers[event.getType()];
|
||||||
if (!previewDef) continue;
|
if (!previewDef) continue;
|
||||||
if (previewDef.isState && isNullOrUndefined(event.getStateKey())) 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 { getSenderName, isSelf } from "./utils";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { MessagePreviewStore } from "../MessagePreviewStore";
|
import type { MessagePreviewStore } from "../MessagePreviewStore";
|
||||||
|
|
||||||
export class ReactionEventPreview implements Preview {
|
export class ReactionEventPreview implements Preview {
|
||||||
|
public constructor(private readonly messagePreviewStore: MessagePreviewStore) {}
|
||||||
|
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null {
|
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null {
|
||||||
const roomId = event.getRoomId();
|
const roomId = event.getRoomId();
|
||||||
if (!roomId) return null; // not a room event
|
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;
|
const relatedEvent = relation.event_id ? room?.findEventById(relation.event_id) : null;
|
||||||
if (!relatedEvent) return null;
|
if (!relatedEvent) return null;
|
||||||
|
|
||||||
const message = MessagePreviewStore.instance.generatePreviewForEvent(relatedEvent);
|
const message = this.messagePreviewStore.generatePreviewForEvent(relatedEvent);
|
||||||
if (isSelf(event)) {
|
if (isSelf(event)) {
|
||||||
return _t("event_preview|m.reaction|you", {
|
return _t("event_preview|m.reaction|you", {
|
||||||
reaction,
|
reaction,
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import {
|
|||||||
EventStatus,
|
EventStatus,
|
||||||
EventTimeline,
|
EventTimeline,
|
||||||
EventType,
|
EventType,
|
||||||
|
M_POLL_KIND_DISCLOSED,
|
||||||
|
M_POLL_START,
|
||||||
|
M_TEXT,
|
||||||
type MatrixClient,
|
type MatrixClient,
|
||||||
type MatrixEvent,
|
type MatrixEvent,
|
||||||
PendingEventOrdering,
|
PendingEventOrdering,
|
||||||
@@ -373,4 +376,82 @@ describe("MessagePreviewStore", () => {
|
|||||||
// @ts-ignore private access
|
// @ts-ignore private access
|
||||||
expect(store.previews.has(nonRenderedRoom.roomId)).toBeFalsy();
|
expect(store.previews.has(nonRenderedRoom.roomId)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("generatePreviewForEvent", () => {
|
||||||
|
it("should generate a preview for a message event", () => {
|
||||||
|
const message = mkMessage({
|
||||||
|
user: "@sender:server",
|
||||||
|
event: true,
|
||||||
|
room: room.roomId,
|
||||||
|
msg: "Hello world",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.generatePreviewForEvent(message)).toBe("Hello world");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate a preview for a sticker event", () => {
|
||||||
|
const sticker = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.Sticker,
|
||||||
|
user: "@sender:server",
|
||||||
|
room: room.roomId,
|
||||||
|
content: { body: "A sticker" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.generatePreviewForEvent(sticker)).toBe("A sticker");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Poll start events have both a stable and an unstable event type, each with its own
|
||||||
|
// registry entry, so exercise both.
|
||||||
|
it.each([M_POLL_START.name, M_POLL_START.altName])(
|
||||||
|
"should generate a preview for a poll start event (%s)",
|
||||||
|
(type) => {
|
||||||
|
const poll = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: type!,
|
||||||
|
user: "@sender:server",
|
||||||
|
room: room.roomId,
|
||||||
|
content: {
|
||||||
|
[M_POLL_START.name]: {
|
||||||
|
question: { [M_TEXT.name]: "Where shall we eat?" },
|
||||||
|
kind: M_POLL_KIND_DISCLOSED.name,
|
||||||
|
answers: [
|
||||||
|
{ id: "pizza", [M_TEXT.name]: "Pizza" },
|
||||||
|
{ id: "sushi", [M_TEXT.name]: "Sushi" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.generatePreviewForEvent(poll)).toBe("Where shall we eat?");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([EventType.CallInvite, EventType.CallAnswer, EventType.CallHangup])(
|
||||||
|
"should generate a preview for a %s event",
|
||||||
|
(type) => {
|
||||||
|
const callEvent = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type,
|
||||||
|
user: "@sender:server",
|
||||||
|
room: room.roomId,
|
||||||
|
content: { call_id: "1" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.generatePreviewForEvent(callEvent)).toBeTruthy();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should return an empty string for an event type without a previewer", () => {
|
||||||
|
const topicEvent = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.RoomTopic,
|
||||||
|
user: "@sender:server",
|
||||||
|
room: room.roomId,
|
||||||
|
content: { topic: "A new topic" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.generatePreviewForEvent(topicEvent)).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-2
@@ -10,12 +10,12 @@ import { RelationType, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
|||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
import { mkEvent, stubClient } from "../../../../test-utils";
|
import { mkEvent, stubClient } from "../../../../test-utils";
|
||||||
// Import directly from the file to avoid circular dependencies with MessagePreviewStore
|
|
||||||
import { ReactionEventPreview } from "../../../../../src/stores/message-preview/previews/ReactionEventPreview";
|
import { ReactionEventPreview } from "../../../../../src/stores/message-preview/previews/ReactionEventPreview";
|
||||||
|
import { MessagePreviewStore } from "../../../../../src/stores/message-preview";
|
||||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||||
|
|
||||||
describe("ReactionEventPreview", () => {
|
describe("ReactionEventPreview", () => {
|
||||||
const preview = new ReactionEventPreview();
|
const preview = new ReactionEventPreview(MessagePreviewStore.instance);
|
||||||
const userId = "@user:example.com";
|
const userId = "@user:example.com";
|
||||||
const roomId = "!room:example.com";
|
const roomId = "!room:example.com";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user