mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Widget permissions customizations using module api (#10121)
* Using module api to customize widget permissions Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net> * Revert type export and use ComponentProps instead. Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net> --------- Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net> Co-authored-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
This commit is contained in:
parent
9a0e537916
commit
7b77f76486
@ -133,6 +133,7 @@ describe("Stickers", () => {
|
||||
type: "m.stickerpicker",
|
||||
name: STICKER_PICKER_WIDGET_NAME,
|
||||
url: stickerPickerUrl,
|
||||
creatorUserId: "@userId",
|
||||
},
|
||||
id: STICKER_PICKER_WIDGET_ID,
|
||||
},
|
||||
|
@ -58,7 +58,7 @@
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/analytics-events": "^0.4.0",
|
||||
"@matrix-org/matrix-wysiwyg": "^1.1.1",
|
||||
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||
"@matrix-org/react-sdk-module-api": "^0.0.4",
|
||||
"@sentry/browser": "^7.0.0",
|
||||
"@sentry/tracing": "^7.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
import React, { useContext } from "react";
|
||||
import { MatrixCapabilities } from "matrix-widget-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
||||
import { ChevronFace } from "../../structures/ContextMenu";
|
||||
@ -34,6 +35,8 @@ import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
|
||||
import { ModuleRunner } from "../../../modules/ModuleRunner";
|
||||
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||
app: IApp;
|
||||
@ -45,7 +48,7 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||
onEditClick?(): void;
|
||||
}
|
||||
|
||||
const WidgetContextMenu: React.FC<IProps> = ({
|
||||
export const WidgetContextMenu: React.FC<IProps> = ({
|
||||
onFinished,
|
||||
app,
|
||||
userWidget,
|
||||
@ -158,12 +161,17 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
const isLocalWidget = WidgetType.JITSI.matches(app.type);
|
||||
let revokeButton;
|
||||
if (!userWidget && !isLocalWidget && isAllowedWidget) {
|
||||
const opts: ApprovalOpts = { approved: undefined };
|
||||
ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(app));
|
||||
|
||||
if (!opts.approved) {
|
||||
const onRevokeClick = (): void => {
|
||||
logger.info("Revoking permission for widget to load: " + app.eventId);
|
||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||
if (app.eventId !== undefined) current[app.eventId] = false;
|
||||
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
|
||||
SettingsStore.setValue("allowedWidgets", roomId, level, current).catch((err) => {
|
||||
if (!level) throw new Error("level must be defined");
|
||||
SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current).catch((err) => {
|
||||
logger.error(err);
|
||||
// We don't really need to do anything about this - the user will just hit the button again.
|
||||
});
|
||||
@ -172,10 +180,12 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
|
||||
revokeButton = <IconizedContextMenuOption onClick={onRevokeClick} label={_t("Revoke permissions")} />;
|
||||
}
|
||||
}
|
||||
|
||||
let moveLeftButton;
|
||||
if (showUnpin && widgetIndex > 0) {
|
||||
const onClick = (): void => {
|
||||
if (!room) throw new Error("room must be defined");
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
|
||||
onFinished();
|
||||
};
|
||||
@ -207,5 +217,3 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetContextMenu;
|
||||
|
@ -23,6 +23,7 @@ import classNames from "classnames";
|
||||
import { MatrixCapabilities } from "matrix-widget-api";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
@ -36,7 +37,7 @@ import { aboveLeftOf, ContextMenuButton } from "../../structures/ContextMenu";
|
||||
import PersistedElement, { getPersistKey } from "./PersistedElement";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
@ -50,6 +51,7 @@ import { Action } from "../../../dispatcher/actions";
|
||||
import { ElementWidgetCapabilities } from "../../../stores/widgets/ElementWidgetCapabilities";
|
||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { ModuleRunner } from "../../../modules/ModuleRunner";
|
||||
|
||||
interface IProps {
|
||||
app: IApp;
|
||||
@ -162,6 +164,9 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
private hasPermissionToLoad = (props: IProps): boolean => {
|
||||
if (this.usingLocalWidget()) return true;
|
||||
if (!props.room) return true; // user widgets always have permissions
|
||||
const opts: ApprovalOpts = { approved: undefined };
|
||||
ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(this.props.app));
|
||||
if (opts.approved) return true;
|
||||
|
||||
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
|
||||
const allowed = props.app.eventId !== undefined && (currentlyAllowedWidgets[props.app.eventId] ?? false);
|
||||
|
@ -40,7 +40,7 @@ import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { UIComponent, UIFeature } from "../../../settings/UIFeature";
|
||||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||
import { useRoomMemberCount } from "../../../hooks/useRoomMembers";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import { usePinnedEvents } from "./PinnedMessagesCard";
|
||||
|
@ -24,7 +24,7 @@ import AppTile from "../elements/AppTile";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useWidgets } from "./RoomSummaryCard";
|
||||
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
|
@ -39,6 +39,11 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||
import { Direction } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
ApprovalOpts,
|
||||
CapabilitiesOpts,
|
||||
WidgetLifecycle,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import SdkConfig, { DEFAULTS } from "../../SdkConfig";
|
||||
import { iterableDiff, iterableIntersection } from "../../utils/iterables";
|
||||
@ -55,6 +60,7 @@ import dis from "../../dispatcher/dispatcher";
|
||||
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
|
||||
import { navigateToPermalink } from "../../utils/permalinks/navigator";
|
||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||
import { ModuleRunner } from "../../modules/ModuleRunner";
|
||||
|
||||
// TODO: Purge this from the universe
|
||||
|
||||
@ -171,15 +177,22 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||
allowedSoFar.add(cap);
|
||||
missing.delete(cap);
|
||||
});
|
||||
|
||||
let approved: Set<string> | undefined;
|
||||
if (WidgetPermissionCustomisations.preapproveCapabilities) {
|
||||
const approved = await WidgetPermissionCustomisations.preapproveCapabilities(this.forWidget, requested);
|
||||
approved = await WidgetPermissionCustomisations.preapproveCapabilities(this.forWidget, requested);
|
||||
} else {
|
||||
const opts: CapabilitiesOpts = { approvedCapabilities: undefined };
|
||||
ModuleRunner.instance.invoke(WidgetLifecycle.CapabilitiesRequest, opts, this.forWidget, requested);
|
||||
approved = opts.approvedCapabilities;
|
||||
}
|
||||
if (approved) {
|
||||
approved.forEach((cap) => {
|
||||
allowedSoFar.add(cap);
|
||||
missing.delete(cap);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Do something when the widget requests new capabilities not yet asked for
|
||||
let rememberApproved = false;
|
||||
if (missing.size > 0) {
|
||||
@ -366,6 +379,15 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||
}
|
||||
|
||||
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>): Promise<void> {
|
||||
const opts: ApprovalOpts = { approved: undefined };
|
||||
ModuleRunner.instance.invoke(WidgetLifecycle.IdentityRequest, opts, this.forWidget);
|
||||
if (opts.approved) {
|
||||
return observer.update({
|
||||
state: OpenIDRequestState.Allowed,
|
||||
token: await MatrixClientPeg.get().getOpenIdToken(),
|
||||
});
|
||||
}
|
||||
|
||||
const oidcState = SdkContextClass.instance.widgetPermissionStore.getOIDCState(
|
||||
this.forWidget,
|
||||
this.forWidgetKind,
|
||||
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2023 Mikhail Aheichyk
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { screen, render } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { MatrixWidgetType } from "matrix-widget-api";
|
||||
import {
|
||||
ApprovalOpts,
|
||||
WidgetInfo,
|
||||
WidgetLifecycle,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import { WidgetContextMenu } from "../../../../src/components/views/context_menus/WidgetContextMenu";
|
||||
import { IApp } from "../../../../src/stores/WidgetStore";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import WidgetUtils from "../../../../src/utils/WidgetUtils";
|
||||
import { ModuleRunner } from "../../../../src/modules/ModuleRunner";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("<WidgetContextMenu />", () => {
|
||||
const widgetId = "w1";
|
||||
const eventId = "e1";
|
||||
const roomId = "r1";
|
||||
const userId = "@user-id:server";
|
||||
|
||||
const app: IApp = {
|
||||
id: widgetId,
|
||||
eventId,
|
||||
roomId,
|
||||
type: MatrixWidgetType.Custom,
|
||||
url: "https://example.com",
|
||||
name: "Example 1",
|
||||
creatorUserId: userId,
|
||||
avatar_url: undefined,
|
||||
};
|
||||
|
||||
const mockClient = {
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
let onFinished: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
onFinished = jest.fn();
|
||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function getComponent(props: Partial<ComponentProps<typeof WidgetContextMenu>> = {}): JSX.Element {
|
||||
return (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<WidgetContextMenu app={app} onFinished={onFinished} {...props} />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
it("renders revoke button", async () => {
|
||||
const { rerender } = render(getComponent());
|
||||
|
||||
const revokeButton = screen.getByLabelText("Revoke permissions");
|
||||
expect(revokeButton).toBeInTheDocument();
|
||||
|
||||
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
|
||||
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === widgetId) {
|
||||
(opts as ApprovalOpts).approved = true;
|
||||
}
|
||||
});
|
||||
|
||||
rerender(getComponent());
|
||||
expect(revokeButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("revokes permissions", async () => {
|
||||
render(getComponent());
|
||||
await userEvent.click(screen.getByLabelText("Revoke permissions"));
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
expect(SettingsStore.getValue("allowedWidgets", roomId)[eventId]).toBe(false);
|
||||
});
|
||||
});
|
@ -23,6 +23,11 @@ import { act, render, RenderResult } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { SpiedFunction } from "jest-mock";
|
||||
import {
|
||||
ApprovalOpts,
|
||||
WidgetInfo,
|
||||
WidgetLifecycle,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import RightPanel from "../../../../src/components/structures/RightPanel";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
@ -44,6 +49,7 @@ import AppsDrawer from "../../../../src/components/views/rooms/AppsDrawer";
|
||||
import { ElementWidgetCapabilities } from "../../../../src/stores/widgets/ElementWidgetCapabilities";
|
||||
import { ElementWidget } from "../../../../src/stores/widgets/StopGapWidget";
|
||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { ModuleRunner } from "../../../../src/modules/ModuleRunner";
|
||||
|
||||
describe("AppTile", () => {
|
||||
let cli: MatrixClient;
|
||||
@ -380,4 +386,21 @@ describe("AppTile", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("for a pinned widget permission load", () => {
|
||||
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
|
||||
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
|
||||
(opts as ApprovalOpts).approved = true;
|
||||
}
|
||||
});
|
||||
|
||||
// userId and creatorUserId are different
|
||||
const renderResult = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
expect(renderResult.queryByRole("button", { name: "Continue" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -18,12 +18,27 @@ import { mocked, MockedObject } from "jest-mock";
|
||||
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "matrix-js-sdk/src/client";
|
||||
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
|
||||
import { Direction, EventType, MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
|
||||
import { Widget, MatrixWidgetType, WidgetKind, WidgetDriver, ITurnServer } from "matrix-widget-api";
|
||||
import {
|
||||
Widget,
|
||||
MatrixWidgetType,
|
||||
WidgetKind,
|
||||
WidgetDriver,
|
||||
ITurnServer,
|
||||
SimpleObservable,
|
||||
OpenIDRequestState,
|
||||
IOpenIDUpdate,
|
||||
} from "matrix-widget-api";
|
||||
import {
|
||||
ApprovalOpts,
|
||||
CapabilitiesOpts,
|
||||
WidgetLifecycle,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { StopGapWidgetDriver } from "../../../src/stores/widgets/StopGapWidgetDriver";
|
||||
import { stubClient } from "../../test-utils";
|
||||
import { ModuleRunner } from "../../../src/modules/ModuleRunner";
|
||||
import dis from "../../../src/dispatcher/dispatcher";
|
||||
|
||||
describe("StopGapWidgetDriver", () => {
|
||||
@ -101,6 +116,44 @@ describe("StopGapWidgetDriver", () => {
|
||||
expect(approvedCapabilities).toEqual(requestedCapabilities);
|
||||
});
|
||||
|
||||
it("approves capabilities via module api", async () => {
|
||||
const driver = mkDefaultDriver();
|
||||
|
||||
const requestedCapabilities = new Set(["org.matrix.msc2931.navigate", "org.matrix.msc2762.timeline:*"]);
|
||||
|
||||
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation(
|
||||
(lifecycleEvent, opts, widgetInfo, requested) => {
|
||||
if (lifecycleEvent === WidgetLifecycle.CapabilitiesRequest) {
|
||||
(opts as CapabilitiesOpts).approvedCapabilities = requested;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const approvedCapabilities = await driver.validateCapabilities(requestedCapabilities);
|
||||
expect(approvedCapabilities).toEqual(requestedCapabilities);
|
||||
});
|
||||
|
||||
it("approves identity via module api", async () => {
|
||||
const driver = mkDefaultDriver();
|
||||
|
||||
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
|
||||
if (lifecycleEvent === WidgetLifecycle.IdentityRequest) {
|
||||
(opts as ApprovalOpts).approved = true;
|
||||
}
|
||||
});
|
||||
|
||||
const listener = jest.fn();
|
||||
const observer = new SimpleObservable<IOpenIDUpdate>();
|
||||
observer.onUpdate(listener);
|
||||
await driver.askOpenID(observer);
|
||||
|
||||
const openIdUpdate: IOpenIDUpdate = {
|
||||
state: OpenIDRequestState.Allowed,
|
||||
token: await client.getOpenIdToken(),
|
||||
};
|
||||
expect(listener).toBeCalledWith(openIdUpdate);
|
||||
});
|
||||
|
||||
describe("sendToDevice", () => {
|
||||
const contentMap = {
|
||||
"@alice:example.org": {
|
||||
|
@ -1598,10 +1598,10 @@
|
||||
version "3.2.14"
|
||||
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984"
|
||||
|
||||
"@matrix-org/react-sdk-module-api@^0.0.3":
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-0.0.3.tgz#a7ac1b18a72d18d08290b81fa33b0d8d00a77d2b"
|
||||
integrity sha512-jQmLhVIanuX0g7Jx1OIqlzs0kp72PfSpv3umi55qVPYcAPQmO252AUs0vncatK8O4e013vohdnNhly19a/kmLQ==
|
||||
"@matrix-org/react-sdk-module-api@^0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-0.0.4.tgz#da71fc2e4c8143e87b5c2bc067ccbc0c146816fe"
|
||||
integrity sha512-4gcgef3Ne9+Ae0bAErK1Swo9FxTZBDEogX/Iu2kcLWWROOKMjmeWL2PkM83ylsxZ32YY6a6ndRqV/SwRmDeJxg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.9"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user