Experimental Module API Additions (#30863)

* Module API experiments

* Move ResizerNotifier into SDKContext

so we don't have to pass it into RoomView

* Add the MultiRoomViewStore

* Make RoomViewStore able to take a roomId prop

* Different interface to add space panel items

A bit less flexible but probably simpler and will help keep things
actually consistent rather than just allowing modules to stick any
JSX into the space panel (which means they also have to worry about
styling if they *do* want it to be consistent).

* Allow space panel items to be updated

and manage which one is selected, allowing module "spaces" to be
considered spaces

* Remove fetchRoomFn from SpaceNotificationStore

which didn't really seem to have any point as it was only called from
one place

* Switch to using module api via .instance

* Fairly awful workaround

to actually break the dependency nightmare

* Add test for multiroomviewstore

* add test

* Make room names deterministic

So the tests don't fail if you add other tests or run them individually

* Add test for builtinsapi

* Update module api

* RVS is not needed as prop anymore

Since it's passed through context

* Add roomId to prop

* Remove RoomViewStore from state

This is now accessed through class field

* Fix test

* No need to pass RVS from LoggedInView

* Add RoomContextType

* Implement new builtins api

* Add tests

* Fix import

* Fix circular dependency issue

* Fix import

* Add more tests

* Improve comment

* room-id is optional

* Update license

* Add implementation for AccountDataApi

* Add implementation for Room

* Add implementation for ClientApi

* Create ClientApi in Api.ts

* Write tests

* Use nullish coalescing assignment

* Implement openRoom in NavigationApi

* Write tests

* Add implementation for StoresApi

* Write tests

* Fix circular dependency

* Add comments in lieu of type

and fix else block

* Change to class field

---------

Co-authored-by: R Midhun Suresh <hi@midhun.dev>
This commit is contained in:
David Baker
2025-11-05 07:24:26 +00:00
committed by GitHub
parent 514dd07a28
commit 42f8247c2e
51 changed files with 1088 additions and 154 deletions

View File

@@ -15,10 +15,9 @@ import RecordingPlayback, {
PlaybackLayout,
} from "../../../../../src/components/views/audio_messages/RecordingPlayback";
import { Playback } from "../../../../../src/audio/Playback";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { type RoomContextType, TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { createAudioContext } from "../../../../../src/audio/compat";
import { flushPromises } from "../../../../test-utils";
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/WorkerManager", () => ({
@@ -54,7 +53,10 @@ describe("<RecordingPlayback />", () => {
const mockChannelData = new Float32Array();
const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File } as IRoomState;
const defaultRoom = {
roomId: "!room:server.org",
timelineRenderingType: TimelineRenderingType.File,
} as RoomContextType;
const getComponent = (props: React.ComponentProps<typeof RecordingPlayback>, room = defaultRoom) =>
render(
<ScopedRoomContextProvider {...room}>

View File

@@ -31,8 +31,7 @@ import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMe
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { ConnectionState } from "../../../../../src/models/Call";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext";
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
import RoomContext from "../../../../../src/contexts/RoomContext";
import RoomContext, { type RoomContextType } from "../../../../../src/contexts/RoomContext";
describe("<RoomCallBanner />", () => {
let client: Mocked<MatrixClient>;
@@ -51,7 +50,7 @@ describe("<RoomCallBanner />", () => {
emit: jest.fn(),
};
let roomContext: IRoomState;
let roomContext: RoomContextType;
beforeEach(() => {
stubClient();
@@ -79,7 +78,7 @@ describe("<RoomCallBanner />", () => {
...RoomContext,
roomId: room.roomId,
roomViewStore: mockRoomViewStore,
} as unknown as IRoomState;
} as unknown as RoomContextType;
});
afterEach(async () => {

View File

@@ -27,8 +27,7 @@ import { mocked } from "jest-mock";
import userEvent from "@testing-library/user-event";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
import { type RoomContextType, TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { canEditContent } from "../../../../../src/utils/EventUtils";
import { copyPlaintext, getSelectedText } from "../../../../../src/utils/strings";
import MessageContextMenu from "../../../../../src/components/views/context_menus/MessageContextMenu";
@@ -711,18 +710,18 @@ describe("MessageContextMenu", () => {
});
});
function createRightClickMenuWithContent(eventContent: object, context?: Partial<IRoomState>): RenderResult {
function createRightClickMenuWithContent(eventContent: object, context?: Partial<RoomContextType>): RenderResult {
return createMenuWithContent(eventContent, { rightClick: true }, context);
}
function createRightClickMenu(mxEvent: MatrixEvent, context?: Partial<IRoomState>): RenderResult {
function createRightClickMenu(mxEvent: MatrixEvent, context?: Partial<RoomContextType>): RenderResult {
return createMenu(mxEvent, { rightClick: true }, context);
}
function createMenuWithContent(
eventContent: object,
props?: Partial<MessageContextMenu["props"]>,
context?: Partial<IRoomState>,
context?: Partial<RoomContextType>,
): RenderResult {
// XXX: We probably shouldn't be assuming all events are going to be message events, but considering this
// test is for the Message context menu, it's a fairly safe assumption.
@@ -739,7 +738,7 @@ function makeDefaultRoom(): Room {
function createMenu(
mxEvent: MatrixEvent,
props?: Partial<MessageContextMenu["props"]>,
context: Partial<IRoomState> = {},
context: Partial<RoomContextType> = {},
beacons: Map<BeaconIdentifier, Beacon> = new Map(),
room: Room = makeDefaultRoom(),
): RenderResult {
@@ -755,7 +754,7 @@ function createMenu(
client.getRoom = jest.fn().mockReturnValue(room);
return render(
<ScopedRoomContextProvider {...(context as IRoomState)}>
<ScopedRoomContextProvider {...(context as RoomContextType)}>
<MessageContextMenu mxEvent={mxEvent} onFinished={jest.fn()} {...props} />
</ScopedRoomContextProvider>,
);

View File

@@ -26,8 +26,7 @@ import {
} from "../../../../test-utils";
import DateSeparator from "../../../../../src/components/views/messages/DateSeparator";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext";
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
import RoomContext from "../../../../../src/contexts/RoomContext";
import RoomContext, { type RoomContextType } from "../../../../../src/contexts/RoomContext";
jest.mock("../../../../../src/settings/SettingsStore");
@@ -50,7 +49,7 @@ describe("DateSeparator", () => {
...RoomContext,
roomId,
roomViewStore: mockRoomViewStore,
} as unknown as IRoomState;
} as unknown as RoomContextType;
const mockClient = getMockClientWithEventEmitter({
timestampToEvent: jest.fn(),

View File

@@ -29,8 +29,7 @@ import {
makeBeaconInfoEvent,
} from "../../../../test-utils";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
import RoomContext, { type RoomContextType, TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import dispatcher from "../../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { Action } from "../../../../../src/dispatcher/actions";
@@ -115,8 +114,8 @@ describe("<MessageActionBar />", () => {
canSendMessages: true,
canReact: true,
room,
} as unknown as IRoomState;
const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) =>
} as unknown as RoomContextType;
const getComponent = (props = {}, roomContext: Partial<RoomContextType> = {}) =>
render(
<ScopedRoomContextProvider {...defaultRoomContext} {...roomContext}>
<MessageActionBar {...defaultProps} {...props} />

View File

@@ -27,12 +27,12 @@ import {
import DocumentOffset from "../../../../../src/editor/offset";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer";
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import Autocompleter, { type IProviderCompletions } from "../../../../../src/autocomplete/Autocompleter";
import NotifProvider from "../../../../../src/autocomplete/NotifProvider";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
describe("<EditMessageComposer/>", () => {
const userId = "@alice:server.org";
@@ -75,7 +75,7 @@ describe("<EditMessageComposer/>", () => {
const defaultRoomContext = getRoomContext(room, {});
const getComponent = (editState: EditorStateTransfer, roomContext: IRoomState = defaultRoomContext) =>
const getComponent = (editState: EditorStateTransfer, roomContext: RoomContextType = defaultRoomContext) =>
render(<EditMessageComposerWithMatrixClient editState={editState} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={mockClient}>

View File

@@ -30,14 +30,13 @@ import { mkEncryptedMatrixEvent } from "matrix-js-sdk/src/testing";
import EventTile, { type EventTileProps } from "../../../../../src/components/views/rooms/EventTile";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { type RoomContextType, TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { filterConsole, flushPromises, getRoomContext, mkEvent, mkMessage, stubClient } from "../../../../test-utils";
import { mkThread } from "../../../../test-utils/threads";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import dis from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
import PinningUtils from "../../../../../src/utils/PinningUtils";
import { Layout } from "../../../../../src/settings/enums/Layout";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
@@ -52,7 +51,7 @@ describe("EventTile", () => {
/** wrap the EventTile up in context providers, and with basic properties, as it would be by MessagePanel normally. */
function WrappedEventTile(props: {
roomContext: IRoomState;
roomContext: RoomContextType;
eventTilePropertyOverrides?: Partial<EventTileProps>;
}) {
return (
@@ -71,7 +70,7 @@ describe("EventTile", () => {
function getComponent(
overrides: Partial<EventTileProps> = {},
renderingType: TimelineRenderingType = TimelineRenderingType.Room,
roomContext: Partial<IRoomState> = {},
roomContext: Partial<RoomContextType> = {},
) {
const context = getRoomContext(room, {
timelineRenderingType: renderingType,

View File

@@ -24,7 +24,6 @@ import {
import MessageComposer from "../../../../../src/components/views/rooms/MessageComposer";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
import ResizeNotifier from "../../../../../src/utils/ResizeNotifier";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import { LocalRoom } from "../../../../../src/models/LocalRoom";
@@ -36,6 +35,7 @@ import { addTextToComposerRTL } from "../../../../test-utils/composer";
import UIStore, { UI_EVENTS } from "../../../../../src/stores/UIStore";
import { Action } from "../../../../../src/dispatcher/actions";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
const openStickerPicker = async (): Promise<void> => {
await userEvent.click(screen.getByLabelText("More options"));
@@ -155,7 +155,7 @@ describe("MessageComposer", () => {
});
describe("when receiving a »reply_to_event«", () => {
let roomContext: IRoomState;
let roomContext: RoomContextType;
let resizeNotifier: ResizeNotifier;
beforeEach(() => {
@@ -458,7 +458,7 @@ function wrapAndRender(
canSendMessages,
tombstone,
narrow,
} as unknown as IRoomState;
} as unknown as RoomContextType;
const defaultProps = {
room,

View File

@@ -11,10 +11,10 @@ import { render, screen, waitFor } from "jest-matrix-react";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { createTestClient, getRoomContext, mkStubRoom } from "../../../../test-utils";
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import MessageComposerButtons from "../../../../../src/components/views/rooms/MessageComposerButtons";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
describe("MessageComposerButtons", () => {
// @ts-ignore - we're deliberately not implementing the whole interface here, but
@@ -50,7 +50,7 @@ describe("MessageComposerButtons", () => {
function wrapAndRender(component: React.ReactElement, narrow: boolean) {
const mockRoom = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any;
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, { narrow });
const defaultRoomContext: RoomContextType = getRoomContext(mockRoom, { narrow });
return render(
<MatrixClientContext.Provider value={mockClient}>

View File

@@ -22,16 +22,16 @@ import {
} from "../../../../test-utils";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import NewRoomIntro from "../../../../../src/components/views/rooms/NewRoomIntro";
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { DirectoryMember } from "../../../../../src/utils/direct-messages";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
const renderNewRoomIntro = (client: MatrixClient, room: Room | LocalRoom) => {
render(
<MatrixClientContext.Provider value={client}>
<ScopedRoomContextProvider {...({ room, roomId: room.roomId } as unknown as IRoomState)}>
<ScopedRoomContextProvider {...({ room, roomId: room.roomId } as unknown as RoomContextType)}>
<NewRoomIntro />
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,

View File

@@ -41,8 +41,7 @@ import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext";
import { type IRoomState } from "../../../../../../src/components/structures/RoomView";
import RoomContext from "../../../../../../src/contexts/RoomContext";
import RoomContext, { type RoomContextType } from "../../../../../../src/contexts/RoomContext";
import RightPanelStore from "../../../../../../src/stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../../../../src/stores/right-panel/RightPanelStorePhases";
import LegacyCallHandler from "../../../../../../src/LegacyCallHandler";
@@ -85,7 +84,7 @@ describe("RoomHeader", () => {
emit: jest.fn(),
};
let roomContext: IRoomState;
let roomContext: RoomContextType;
function getWrapper(): RenderOptions {
return {
@@ -121,7 +120,7 @@ describe("RoomHeader", () => {
...RoomContext,
roomId: ROOM_ID,
roomViewStore: mockRoomViewStore,
} as unknown as IRoomState;
} as unknown as RoomContextType;
});
afterEach(() => {

View File

@@ -17,7 +17,7 @@ import SendMessageComposer, {
isQuickReaction,
} from "../../../../../src/components/views/rooms/SendMessageComposer";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { type RoomContextType, TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import EditorModel from "../../../../../src/editor/model";
import { createPartCreator } from "../../../editor/mock";
import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../../test-utils";
@@ -25,7 +25,7 @@ import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import DocumentOffset from "../../../../../src/editor/offset";
import { Layout } from "../../../../../src/settings/enums/Layout";
import { type IRoomState, MainSplitContentType } from "../../../../../src/components/structures/RoomView";
import { MainSplitContentType } from "../../../../../src/components/structures/RoomView";
import { mockPlatformPeg } from "../../../../test-utils/platform";
import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room";
import { addTextToComposer } from "../../../../test-utils/composer";
@@ -37,7 +37,7 @@ jest.mock("../../../../../src/utils/local-room", () => ({
}));
describe("<SendMessageComposer/>", () => {
const defaultRoomContext: IRoomState = {
const defaultRoomContext: RoomContextType = {
roomViewStore: SdkContextClass.instance.roomViewStore,
roomLoading: true,
peekLoading: false,

View File

@@ -9,8 +9,8 @@ Please see LICENSE files in the repository root for full details.
import { type EventTimeline, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { getRoomContext, mkEvent, mkStubRoom, stubClient } from "../../../../../test-utils";
import { type IRoomState } from "../../../../../../src/components/structures/RoomView";
import EditorStateTransfer from "../../../../../../src/utils/EditorStateTransfer";
import type { RoomContextType } from "../../../../../../src/contexts/RoomContext";
export function createMocks(eventContent = "Replying <strong>to</strong> this new content") {
const mockClient = stubClient();
@@ -31,7 +31,7 @@ export function createMocks(eventContent = "Replying <strong>to</strong> this ne
return eventId === mockEvent.getId() ? mockEvent : null;
});
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {
const defaultRoomContext: RoomContextType = getRoomContext(mockRoom, {
liveTimeline: { getEvents: (): MatrixEvent[] => [] } as unknown as EventTimeline,
});