Skip to content

Commit

Permalink
Use mapped types around account data events (#28752)
Browse files Browse the repository at this point in the history
* Use mapped types around account data events

---------

Signed-off-by: Michael Telatynski <[email protected]>
  • Loading branch information
t3chguy authored Dec 19, 2024
1 parent baaed75 commit be181d2
Show file tree
Hide file tree
Showing 25 changed files with 152 additions and 71 deletions.
2 changes: 1 addition & 1 deletion playwright/e2e/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ test.describe("Cryptography", function () {
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
* @param keyType
*/
async function verifyKey(app: ElementAppPage, keyType: string) {
async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") {
const accountData: { encrypted: Record<string, Record<string, string>> } = await app.client.evaluate(
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
keyType,
Expand Down
3 changes: 2 additions & 1 deletion playwright/e2e/integration-manager/get-openid-token.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";

const ROOM_NAME = "Integration Manager Test";

Expand Down Expand Up @@ -92,7 +93,7 @@ test.describe("Integration Manager: Get OpenID Token", () => {
},
},
id: "integration-manager",
},
} as unknown as UserWidget,
});

// Succeed when checking the token is valid
Expand Down
3 changes: 2 additions & 1 deletion playwright/e2e/integration-manager/kick.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";

const ROOM_NAME = "Integration Manager Test";
const USER_DISPLAY_NAME = "Alice";
Expand Down Expand Up @@ -136,7 +137,7 @@ test.describe("Integration Manager: Kick", () => {
},
},
id: "integration-manager",
},
} as unknown as UserWidget,
});

// Succeed when checking the token is valid
Expand Down
3 changes: 2 additions & 1 deletion playwright/e2e/integration-manager/read_events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";

const ROOM_NAME = "Integration Manager Test";

Expand Down Expand Up @@ -107,7 +108,7 @@ test.describe("Integration Manager: Read Events", () => {
},
},
id: "integration-manager",
},
} as unknown as UserWidget,
});

// Succeed when checking the token is valid
Expand Down
3 changes: 2 additions & 1 deletion playwright/e2e/integration-manager/send_event.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";

const ROOM_NAME = "Integration Manager Test";

Expand Down Expand Up @@ -113,7 +114,7 @@ test.describe("Integration Manager: Send Event", () => {
},
},
id: "integration-manager",
},
} as unknown as UserWidget,
});

// Succeed when checking the token is valid
Expand Down
4 changes: 2 additions & 2 deletions playwright/e2e/room/room.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import type { EventType } from "matrix-js-sdk/src/matrix";
import type { AccountDataEvents } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test";
import { Bot } from "../../pages/bot";

Expand All @@ -28,7 +28,7 @@ test.describe("Room Directory", () => {
const charlieRoom = await cli.createRoom({ is_direct: true });
await cli.invite(bobRoom.room_id, bob);
await cli.invite(charlieRoom.room_id, charlie);
await cli.setAccountData("m.direct" as EventType, {
await cli.setAccountData("m.direct" as keyof AccountDataEvents, {
[bob]: [bobRoom.room_id],
[charlie]: [charlieRoom.room_id],
});
Expand Down
5 changes: 4 additions & 1 deletion playwright/e2e/spotlight/spotlight.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import type { AccountDataEvents } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test";
import { Filter } from "../../pages/Spotlight";
import { Bot } from "../../pages/bot";
Expand Down Expand Up @@ -255,7 +256,9 @@ test.describe("Spotlight", () => {

// Invite BotBob into existing DM with ByteBot
const dmRooms = await app.client.evaluate((client, userId) => {
const map = client.getAccountData("m.direct")?.getContent<Record<string, string[]>>();
const map = client
.getAccountData("m.direct" as keyof AccountDataEvents)
?.getContent<Record<string, string[]>>();
return map[userId] ?? [];
}, bot2UserId);
expect(dmRooms).toHaveLength(1);
Expand Down
3 changes: 2 additions & 1 deletion playwright/e2e/widgets/stickers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Credentials } from "../../plugins/homeserver";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";

const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";
const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
Expand Down Expand Up @@ -123,7 +124,7 @@ async function setWidgetAccountData(
state_key: STICKER_PICKER_WIDGET_ID,
type: "m.widget",
id: STICKER_PICKER_WIDGET_ID,
},
} as unknown as UserWidget,
});
}

Expand Down
8 changes: 6 additions & 2 deletions playwright/pages/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
Upload,
StateEvents,
TimelineEvents,
AccountDataEvents,
} from "matrix-js-sdk/src/matrix";
import type { RoomMessageEventContent } from "matrix-js-sdk/src/types";
import { Credentials } from "../plugins/homeserver";
Expand Down Expand Up @@ -439,11 +440,14 @@ export class Client {
* @param type The type of account data to set
* @param content The content to set
*/
public async setAccountData(type: string, content: IContent): Promise<void> {
public async setAccountData<T extends keyof AccountDataEvents>(
type: T,
content: AccountDataEvents[T],
): Promise<void> {
const client = await this.prepareClient();
return client.evaluate(
async (client, { type, content }) => {
await client.setAccountData(type, content);
await client.setAccountData(type as T, content as AccountDataEvents[T]);
},
{ type, content },
);
Expand Down
31 changes: 31 additions & 0 deletions src/@types/matrix-js-sdk.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type { BLURHASH_FIELD } from "../utils/image-media";
import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types";
import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types";
import type { EncryptedFile } from "matrix-js-sdk/src/types";
import type { DeviceClientInformation } from "../utils/device/types.ts";
import type { UserWidget } from "../utils/WidgetUtils-types.ts";

// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
declare module "matrix-js-sdk/src/types" {
Expand Down Expand Up @@ -57,6 +59,35 @@ declare module "matrix-js-sdk/src/types" {
};
}

export interface AccountDataEvents {
// Analytics account data event
"im.vector.analytics": {
id: string;
pseudonymousAnalyticsOptIn?: boolean;
};
// Device client information account data event
[key: `io.element.matrix_client_information.${string}`]: DeviceClientInformation;
// Element settings account data events
"im.vector.setting.breadcrumbs": { recent_rooms: string[] };
"io.element.recent_emoji": { recent_emoji: string[] };
"im.vector.setting.integration_provisioning": { enabled: boolean };
"im.vector.riot.breadcrumb_rooms": { recent_rooms: string[] };
"im.vector.web.settings": Record<string, any>;

// URL preview account data event
"org.matrix.preview_urls": { disable: boolean };

// This is not yet in the Matrix spec yet is being used as if it was
"m.widgets": {
[widgetId: string]: UserWidget;
};

// This is not in the Matrix spec yet seems to use an `m.` prefix
"m.accepted_terms": {
accepted: string[];
};
}

export interface AudioContent {
// MSC1767 + Ideals of MSC2516 as MSC3245
// https://github.com/matrix-org/matrix-doc/pull/3245
Expand Down
3 changes: 2 additions & 1 deletion src/components/structures/LoggedInView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
IUsageLimit,
SyncStateData,
SyncState,
EventType,
} from "matrix-js-sdk/src/matrix";
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import classNames from "classnames";
Expand Down Expand Up @@ -161,7 +162,7 @@ class LoggedInView extends React.Component<IProps, IState> {

this._matrixClient.on(ClientEvent.AccountData, this.onAccountData);
// check push rules on start up as well
monitorSyncedPushRules(this._matrixClient.getAccountData("m.push_rules"), this._matrixClient);
monitorSyncedPushRules(this._matrixClient.getAccountData(EventType.PushRules), this._matrixClient);
this._matrixClient.on(ClientEvent.Sync, this.onSync);
// Call `onSync` with the current state as well
this.onSync(this._matrixClient.getSyncState(), null, this._matrixClient.getSyncStateData() ?? undefined);
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/dialogs/devtools/AccountData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/

import React, { useContext, useMemo, useState } from "react";
import { IContent, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { AccountDataEvents, IContent, MatrixEvent } from "matrix-js-sdk/src/matrix";

Check failure on line 11 in src/components/views/dialogs/devtools/AccountData.tsx

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Module '"matrix-js-sdk/src/matrix"' has no exported member 'AccountDataEvents'.

import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
Expand All @@ -21,7 +21,7 @@ export const AccountDataEventEditor: React.FC<IEditorProps> = ({ mxEvent, onBack

const fields = useMemo(() => [eventTypeField(mxEvent?.getType())], [mxEvent]);

const onSend = async ([eventType]: string[], content?: IContent): Promise<void> => {
const onSend = async ([eventType]: Array<keyof AccountDataEvents>, content?: IContent): Promise<void> => {
await cli.setAccountData(eventType, content || {});

Check failure on line 25 in src/components/views/dialogs/devtools/AccountData.tsx

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Argument of type 'string | number | symbol' is not assignable to parameter of type 'string'.
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/views/settings/devices/useOwnDevices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const useOwnDevices = (): DevicesState => {

const notificationSettings = new Map<string, LocalNotificationSettings>();
Object.keys(devices).forEach((deviceId) => {
const eventType = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
const eventType = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}` as const;
const event = matrixClient.getAccountData(eventType);
if (event) {
notificationSettings.set(deviceId, event.getContent());
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useAccountData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ Please see LICENSE files in the repository root for full details.
*/

import { useCallback, useState } from "react";
import { ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { AccountDataEvents, ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";

Check failure on line 10 in src/hooks/useAccountData.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Module '"matrix-js-sdk/src/matrix"' has no exported member 'AccountDataEvents'.

import { useTypedEventEmitter } from "./useEventEmitter";

const tryGetContent = <T extends {}>(ev?: MatrixEvent): T | undefined => ev?.getContent<T>();

// Hook to simplify listening to Matrix account data
export const useAccountData = <T extends {}>(cli: MatrixClient, eventType: string): T => {
export const useAccountData = <T extends {}>(cli: MatrixClient, eventType: keyof AccountDataEvents): T => {
const [value, setValue] = useState<T | undefined>(() => tryGetContent<T>(cli.getAccountData(eventType)));

Check failure on line 18 in src/hooks/useAccountData.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Argument of type 'string | number | symbol' is not assignable to parameter of type 'string'.

const handler = useCallback(
Expand Down
17 changes: 9 additions & 8 deletions src/settings/handlers/AccountSettingsHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { AccountDataEvents, ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";

Check failure on line 10 in src/settings/handlers/AccountSettingsHandler.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Module '"matrix-js-sdk/src/matrix"' has no exported member 'AccountDataEvents'.
import { defer } from "matrix-js-sdk/src/utils";
import { isEqual } from "lodash";

Expand Down Expand Up @@ -140,11 +140,11 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
}

// helper function to set account data then await it being echoed back
private async setAccountData(
eventType: string,
field: string,
value: any,
legacyEventType?: string,
private async setAccountData<K extends keyof AccountDataEvents, F extends keyof AccountDataEvents[K]>(
eventType: K,
field: F,
value: AccountDataEvents[K][F],
legacyEventType?: keyof AccountDataEvents,
): Promise<void> {
let content = this.getSettings(eventType);
if (legacyEventType && !content?.[field]) {
Expand All @@ -161,7 +161,8 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
// which race between different lines.
const deferred = defer<void>();
const handler = (event: MatrixEvent): void => {
if (event.getType() !== eventType || !isEqual(event.getContent()[field], value)) return;
if (event.getType() !== eventType || !isEqual(event.getContent<AccountDataEvents[K]>()[field], value))
return;
this.client.off(ClientEvent.AccountData, handler);
deferred.resolve();
};
Expand Down Expand Up @@ -212,7 +213,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
return this.client && !this.client.isGuest();
}

private getSettings(eventType = "im.vector.web.settings"): any {
private getSettings(eventType: keyof AccountDataEvents = "im.vector.web.settings"): any {
// TODO: [TS] Types on return
if (!this.client) return null;

Expand Down
10 changes: 2 additions & 8 deletions src/stores/WidgetStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,11 @@ import WidgetEchoStore from "../stores/WidgetEchoStore";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import WidgetUtils from "../utils/WidgetUtils";
import { UPDATE_EVENT } from "./AsyncStore";
import { IApp } from "../utils/WidgetUtils-types";

interface IState {}

export interface IApp extends IWidget {
"roomId": string;
"eventId"?: string; // not present on virtual widgets
// eslint-disable-next-line camelcase
"avatar_url"?: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
// Whether the widget was created from `widget_build_url` and thus is a call widget of some kind
"io.element.managed_hybrid"?: boolean;
}
export type { IApp };

export function isAppWidget(widget: IWidget | IApp): widget is IApp {
return "roomId" in widget && typeof widget.roomId === "string";
Expand Down
2 changes: 1 addition & 1 deletion src/utils/IdentityServerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function setToDefaultIdentityServer(matrixClient: MatrixClient): void {
const url = getDefaultIdentityServerUrl();
// Account data change will update localstorage, client, etc through dispatcher
matrixClient.setAccountData("m.identity_server", {
base_url: url,
base_url: url ?? null,
});
}

Expand Down
32 changes: 32 additions & 0 deletions src/utils/WidgetUtils-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2017-2020 The Matrix.org Foundation C.I.C.
Copyright 2019 Travis Ralston
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { IWidget } from "matrix-widget-api";

export interface IApp extends IWidget {
"roomId": string;
"eventId"?: string; // not present on virtual widgets
// eslint-disable-next-line camelcase
"avatar_url"?: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
// Whether the widget was created from `widget_build_url` and thus is a call widget of some kind
"io.element.managed_hybrid"?: boolean;
}

export interface IWidgetEvent {
id: string;
type: string;
sender: string;
// eslint-disable-next-line camelcase
state_key: string;
content: IApp;
}

export interface UserWidget extends Omit<IWidgetEvent, "content"> {
content: IWidget & Partial<IApp>;
}
14 changes: 2 additions & 12 deletions src/utils/WidgetUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,13 @@ import WidgetStore, { IApp, isAppWidget } from "../stores/WidgetStore";
import { parseUrl } from "./UrlUtils";
import { useEventEmitter } from "../hooks/useEventEmitter";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import { IWidgetEvent, UserWidget } from "./WidgetUtils-types";

// How long we wait for the state event echo to come back from the server
// before waitFor[Room/User]Widget rejects its promise
const WIDGET_WAIT_TIME = 20000;

export interface IWidgetEvent {
id: string;
type: string;
sender: string;
// eslint-disable-next-line camelcase
state_key: string;
content: IApp;
}

export interface UserWidget extends Omit<IWidgetEvent, "content"> {
content: IWidget & Partial<IApp>;
}
export type { IWidgetEvent, UserWidget };

export default class WidgetUtils {
/**
Expand Down
Loading

0 comments on commit be181d2

Please sign in to comment.