From de97ef7f3ed721fb4e23b51a50b582cc8260708d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 5 Apr 2022 17:54:57 -0600 Subject: [PATCH 1/7] Early structures for a module API surface --- src/ModuleApi.ts | 35 ++++++++++++++++++++++++ src/RuntimeModule.ts | 41 +++++++++++++++++++++++++++++ src/lifecycles/RoomViewLifecycle.ts | 28 ++++++++++++++++++++ src/lifecycles/types.ts | 21 +++++++++++++++ src/types/translations.ts | 21 +++++++++++++++ 5 files changed, 146 insertions(+) create mode 100644 src/ModuleApi.ts create mode 100644 src/RuntimeModule.ts create mode 100644 src/lifecycles/RoomViewLifecycle.ts create mode 100644 src/lifecycles/types.ts create mode 100644 src/types/translations.ts diff --git a/src/ModuleApi.ts b/src/ModuleApi.ts new file mode 100644 index 0000000..07801bf --- /dev/null +++ b/src/ModuleApi.ts @@ -0,0 +1,35 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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 { TranslationStringsObject } from "./types/translations"; + +export interface ModuleApi { + /** + * Register strings with the translation engine. This supports overriding strings which + * the system is already aware of. + * @param translations The translations to load. + */ + registerTranslations(translations: TranslationStringsObject): void; + + /** + * Runs a string through the translation engine. If variables are needed, use %(varName)s + * as a placeholder for varName in the variables object. + * @param s The string. Should already be known to the engine. + * @param variables The variables to replace, if any. + * @returns The translated string. + */ + translateString(s: string, variables?: Record): string; +} diff --git a/src/RuntimeModule.ts b/src/RuntimeModule.ts new file mode 100644 index 0000000..08e6c6f --- /dev/null +++ b/src/RuntimeModule.ts @@ -0,0 +1,41 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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 { EventEmitter } from "events"; +import { ModuleApi } from "./ModuleApi"; + +// TODO: @@ Type the event emitter with AnyLifecycle + +/** + * Represents a module which is loaded at runtime. Modules which implement this class + * will be provided information about the application state and can react to it. + */ +export abstract class RuntimeModule extends EventEmitter { + protected constructor(protected readonly moduleApi: ModuleApi) { + super(); + } + + /** + * Run a string through the translation engine. Shortcut to ModuleApi#translateString(). + * @param s The string. + * @param variables The variables, if any. + * @returns The translated string. + * @protected + */ + protected t(s: string, variables?: Record): string { + return this.moduleApi.translateString(s, variables); + } +} diff --git a/src/lifecycles/RoomViewLifecycle.ts b/src/lifecycles/RoomViewLifecycle.ts new file mode 100644 index 0000000..1fe46ad --- /dev/null +++ b/src/lifecycles/RoomViewLifecycle.ts @@ -0,0 +1,28 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +export enum RoomViewLifecycle { + PreviewRoomNotLoggedIn = "preview_not_logged_in", + JoinFromRoomPreview = "try_join_not_logged_in", +} + +export type RoomPreviewOpts = { + canJoin: boolean; +}; + +export type RoomPreviewListener = (opts: RoomPreviewOpts, roomId: string) => void; + +export type JoinFromPreviewListener = (roomId: string) => void; diff --git a/src/lifecycles/types.ts b/src/lifecycles/types.ts new file mode 100644 index 0000000..3f0a98e --- /dev/null +++ b/src/lifecycles/types.ts @@ -0,0 +1,21 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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 { RoomViewLifecycle } from "./RoomViewLifecycle"; + +export type AnyLifecycle = + | RoomViewLifecycle + ; diff --git a/src/types/translations.ts b/src/types/translations.ts new file mode 100644 index 0000000..925b0ea --- /dev/null +++ b/src/types/translations.ts @@ -0,0 +1,21 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +export type TranslationStringsObject = { + [str: string]: { + [lang: string]: string; + }; +}; From 744641217b24b3cf1cf85cc2b9a57d02d1a042b3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 6 Apr 2022 14:17:28 -0600 Subject: [PATCH 2/7] Add public interface for what an ILAG module would need --- .babelrc | 4 +- package.json | 8 ++- src/ModuleApi.ts | 38 +++++++++++++ src/components/DialogContent.tsx | 61 ++++++++++++++++++++ src/components/Spinner.tsx | 29 ++++++++++ src/components/TextInputField.tsx | 41 +++++++++++++ src/types/credentials.ts | 22 +++++++ tsconfig.build.json | 6 +- tsconfig.json | 4 +- yarn.lock | 95 +++++++++++++++++++++++++++++++ 10 files changed, 303 insertions(+), 5 deletions(-) create mode 100644 src/components/DialogContent.tsx create mode 100644 src/components/Spinner.tsx create mode 100644 src/components/TextInputField.tsx create mode 100644 src/types/credentials.ts diff --git a/.babelrc b/.babelrc index 028dc10..657fe6e 100644 --- a/.babelrc +++ b/.babelrc @@ -2,9 +2,11 @@ "sourceMaps": true, "presets": [ "@babel/preset-env", + "@babel/preset-react", "@babel/preset-typescript" ], "plugins": [ - "@babel/plugin-proposal-class-properties" + "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-runtime" ] } diff --git a/package.json b/package.json index e442fb5..36ee448 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clean": "rimraf lib", "build": "yarn clean && yarn build:compile && yarn build:types", "build:types": "tsc -p ./tsconfig.build.json --emitDeclarationOnly", - "build:compile": "babel -d lib --verbose --extensions \".ts\" src", + "build:compile": "babel -d lib --verbose --extensions \".ts,.tsx\" src", "start": "tsc -p ./tsconfig.build.json -w", "test": "jest", "lint": "eslint src test && tsc --noEmit", @@ -30,9 +30,12 @@ "@babel/eslint-parser": "^7.17.0", "@babel/eslint-plugin": "^7.17.7", "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-transform-runtime": "^7.17.0", "@babel/preset-env": "^7.16.11", + "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.16.7", "@types/jest": "^27.4.1", + "@types/react": "^17", "@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/parser": "^5.18.0", "eslint": "^8.12.0", @@ -43,5 +46,8 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.4", "typescript": "^4.6.3" + }, + "dependencies": { + "@babel/runtime": "^7.17.9" } } diff --git a/src/ModuleApi.ts b/src/ModuleApi.ts index 07801bf..d1c5c2f 100644 --- a/src/ModuleApi.ts +++ b/src/ModuleApi.ts @@ -15,6 +15,9 @@ limitations under the License. */ import { TranslationStringsObject } from "./types/translations"; +import { DialogProps } from "./components/DialogContent"; +import { AccountCredentials } from "./types/credentials"; +import React from "react"; export interface ModuleApi { /** @@ -32,4 +35,39 @@ export interface ModuleApi { * @returns The translated string. */ translateString(s: string, variables?: Record): string; + + /** + * Opens a dialog in the client. + * @param title The title of the dialog + * @param body The function which creates a body component for the dialog. + * @returns Whether the user submitted the dialog or closed it, and the model returned by the + * dialog component if submitted. + */ + // TODO: @@ Support input props to DialogContent + openDialog(title: string, body: (props: P, ref: React.RefObject) => React.ReactNode): Promise<{ didSubmit: boolean, model: M }>; + + /** + * Registers for an account on the currently connected homeserver. + * @param username The username to register. + * @param password The password to register. + * @param displayName Optional display name to set. + * @returns Resolves to the credentials for the created account. + */ + registerAccount(username: string, password: string, displayName?: string): Promise; + + /** + * Switches the user's currently logged-in account to the one specified. The user will not + * be warned. + * @param credentials The credentials to log in with. + * @returns Resolves when complete. + */ + useAccount(credentials: AccountCredentials): Promise; + + /** + * Switches the user's current view to look at the given room, joining it if required. + * @param roomId The room ID to look at. + * @param andJoin True to also join the room if needed. + * @returns Resolves when complete. + */ + switchToRoom(roomId: string, andJoin?: boolean): Promise; } diff --git a/src/components/DialogContent.tsx b/src/components/DialogContent.tsx new file mode 100644 index 0000000..6b03979 --- /dev/null +++ b/src/components/DialogContent.tsx @@ -0,0 +1,61 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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 * as React from "react"; +import { ModuleApi } from "../ModuleApi"; + +export interface DialogProps { + moduleApi: ModuleApi; +} + +export interface DialogState { + busy: boolean; + error?: string; +} + +export abstract class DialogContent

+ extends React.PureComponent { + + protected constructor(props: P, state?: S) { + super(props); + + this.state = { + busy: false, + ...state, + }; + } + + /** + * Run a string through the translation engine. Shortcut to ModuleApi#translateString(). + * @param s The string. + * @param variables The variables, if any. + * @returns The translated string. + * @protected + */ + protected t(s: string, variables?: Record): string { + return this.props.moduleApi.translateString(s, variables); + } + + /** + * Called when the dialog is submitted. Note that calling this will not submit the + * dialog by default - this component will be wrapped in a form which handles keyboard + * submission and buttons on its own. + * + * If the returned promise resolves then the dialog will be closed, otherwise the dialog + * will stay open. + */ + public abstract trySubmit(): Promise; +} diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx new file mode 100644 index 0000000..1e599ad --- /dev/null +++ b/src/components/Spinner.tsx @@ -0,0 +1,29 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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 * as React from "react"; + +export class Spinner extends React.PureComponent { + /** + * The factory this component uses to render itself. Set to a different value to override. + * @returns The component, rendered. + */ + public static renderFactory = (): React.ReactNode => null; + + public render() { + return Spinner.renderFactory(); + } +} diff --git a/src/components/TextInputField.tsx b/src/components/TextInputField.tsx new file mode 100644 index 0000000..fcf5fb2 --- /dev/null +++ b/src/components/TextInputField.tsx @@ -0,0 +1,41 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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 * as React from "react"; + +export interface TextInputFieldProps { + label: string; + value: string; + onChange: (newValue: string) => void; +} + +export class TextInputField extends React.PureComponent { + /** + * The factory this component uses to render itself. Set to a different value to override. + * @param props The component properties + * @returns The component, rendered. + */ + public static renderFactory = (props: TextInputFieldProps): React.ReactNode => ( + + ); + + public render() { + return TextInputField.renderFactory(this.props); + } +} diff --git a/src/types/credentials.ts b/src/types/credentials.ts new file mode 100644 index 0000000..798e7f0 --- /dev/null +++ b/src/types/credentials.ts @@ -0,0 +1,22 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +export interface AccountCredentials { + userId: string; + deviceId: string; + accessToken: string; + homeserverUrl: string; +} diff --git a/tsconfig.build.json b/tsconfig.build.json index eccb5a2..080e76e 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -7,9 +7,11 @@ "module": "commonjs", "emitDecoratorMetadata": true, "declaration": true, - "outDir": "./lib" + "outDir": "./lib", + "jsx": "react" }, "include": [ - "./src/**/*.ts" + "./src/**/*.ts", + "./src/**/*.tsx" ] } diff --git a/tsconfig.json b/tsconfig.json index 8db086b..b52c58b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,10 +7,12 @@ "moduleResolution": "node", "noImplicitAny": true, "declaration": false, - "noEmit": true + "noEmit": true, + "jsx": "react" }, "include": [ "./src/**/*.ts", + "./src/**/*.tsx", "./test/**/*.ts" ] } diff --git a/yarn.lock b/yarn.lock index fdf301f..737f13e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -505,6 +505,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-jsx@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz#50b6571d13f764266a113d77c82b4a6508bbe665" + integrity sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -753,6 +760,39 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-react-display-name@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.16.7.tgz#7b6d40d232f4c0f550ea348593db3b21e2404340" + integrity sha512-qgIg8BcZgd0G/Cz916D5+9kqX0c7nPZyXaP8R2tLNN5tkyIZdG5fEwBrxwplzSnjC1jvQmyMNVwUCZPcbGY7Pg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-react-jsx-development@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz#43a00724a3ed2557ed3f276a01a929e6686ac7b8" + integrity sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.16.7" + +"@babel/plugin-transform-react-jsx@^7.16.7": + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz#eac1565da176ccb1a715dae0b4609858808008c1" + integrity sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-jsx" "^7.16.7" + "@babel/types" "^7.17.0" + +"@babel/plugin-transform-react-pure-annotations@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.16.7.tgz#232bfd2f12eb551d6d7d01d13fe3f86b45eb9c67" + integrity sha512-hs71ToC97k3QWxswh2ElzMFABXHvGiJ01IB1TbYQDGeWRKWz/MPUTh5jGExdHvosYKpnJW5Pm3S4+TA3FyX+GA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-regenerator@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz#9e7576dc476cb89ccc5096fff7af659243b4adeb" @@ -767,6 +807,18 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-runtime@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.0.tgz#0a2e08b5e2b2d95c4b1d3b3371a2180617455b70" + integrity sha512-fr7zPWnKXNc1xoHfrIU9mN/4XKX4VLZ45Q+oMhfsYIaHvg7mHgmhfOy/ckRWqDK7XF3QDigRpkh5DKq6+clE8A== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + babel-plugin-polyfill-corejs2 "^0.3.0" + babel-plugin-polyfill-corejs3 "^0.5.0" + babel-plugin-polyfill-regenerator "^0.3.0" + semver "^6.3.0" + "@babel/plugin-transform-shorthand-properties@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz#e8549ae4afcf8382f711794c0c7b6b934c5fbd2a" @@ -918,6 +970,18 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" +"@babel/preset-react@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.16.7.tgz#4c18150491edc69c183ff818f9f2aecbe5d93852" + integrity sha512-fWpyI8UM/HE6DfPBzD8LnhQ/OcH8AgTaqcqP2nGOXEUV+VKBR5JRN9hCk9ai+zQQ57vtm9oWeXguBCPNUjytgA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-transform-react-display-name" "^7.16.7" + "@babel/plugin-transform-react-jsx" "^7.16.7" + "@babel/plugin-transform-react-jsx-development" "^7.16.7" + "@babel/plugin-transform-react-pure-annotations" "^7.16.7" + "@babel/preset-typescript@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz#ab114d68bb2020afc069cd51b37ff98a046a70b9" @@ -927,6 +991,13 @@ "@babel/helper-validator-option" "^7.16.7" "@babel/plugin-transform-typescript" "^7.16.7" +"@babel/runtime@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" + integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.8.4": version "7.17.8" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" @@ -1336,6 +1407,25 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.4.tgz#5d9b63132df54d8909fce1c3f8ca260fdd693e17" integrity sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA== +"@types/prop-types@*": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + +"@types/react@^17": + version "17.0.43" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55" + integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" @@ -1892,6 +1982,11 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +csstype@^3.0.2: + version "3.0.11" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" + integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw== + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" From f47cdd4a477513f1f09282ce79cb47b1a5875cec Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 12 May 2022 17:03:57 -0600 Subject: [PATCH 3/7] Add some docs --- README.md | 121 +++++++++++++++++++++++++++++++++++--- src/ModuleApi.ts | 20 ++++++- src/RuntimeModule.ts | 2 +- src/types/credentials.ts | 15 +++++ src/types/translations.ts | 5 ++ 5 files changed, 152 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f7f5020..c15c3a0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,117 @@ # matrix-react-sdk-module-api -Proof of concept API surface for writing Modules for the react-sdk -## TODO +API surface for interacting with the [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk) in a safe +and predictable way. -* [ ] Write a better intro/readme -* [ ] Proof of concept -* [ ] If approved, make it a real npm package -* [ ] If approved, fix access controls -* [ ] If approved, maintain this +Modules are simply additional functionality added at compile time for the application and can do things like register +custom translations, translation overrides, open dialogs, and add/modify UI. + +**Note**: This project is still considered alpha/beta quality due to the API surface not being extensive. Please reach +out in [#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) on Matrix for guidance on how to add to +this API surface. + +In general, new code should target a generalized interface. An example would be the `openDialog()` function: while the +first module to use it didn't need custom `props`, it is expected that a dialog would at some point, so we expose it. +On the other hand, we deliberately do not expose the complexity of the react-sdk's dialog stack to this layer until +we need it. We might choose to open sticky dialogs with a new `openStickyDialog()` function instead of appending more +arguments to the existing function. + +## Using the API + +Modules are simply standalone npm packages which get installed/included in the app at compile time. To start, we +recommend using a simple module as a template, such as [element-web-ilag-module](https://github.com/vector-im/element-web-ilag-module). + +The package's `main` entrypoint MUST point to an instance of `RuntimeModule`. That class must be a `default` export +for the module loader to reference correctly. + +The `RuntimeModule` instance MUST have a constructor which accepts a single `ModuleApi` parameter. This is supplied +to the `super()` constructor. + +Otherwise, simply `npm install --save @matrix-org/react-sdk-module-api` and start coding! + +### Custom translations / string overrides + +Custom translation strings (used within your module) or string overrides can be specified using the `registerTranslations` +function on a `ModuleApi` instance. For example: + +```typescript +this.moduleApi.registerTranslations({ + // If you use the translation utilities within your module, register your strings + "My custom string": { + "en": "My custom string", + "fr": "Ma chaîne personnalisée", + }, + + // If you want to override a string already in the app, such as the power level role + // names, use the base string here and redefine the values for each applicable language. + "A string that might already exist in the app": { + "en": "Replacement value for that string", + "fr": "Valeur de remplacement pour cette chaîne", + }, +}); +``` + +If you are within a class provided by the module API then translations are generally accessible with `this.t("my string")`. +This is a shortcut to `this.moduleApi.translateString()` which in turn calls into the translation engine at runtime to +determine which appropriately-translated string should be returned. + +### Opening dialogs + +Dialogs are opened through the `openDialog()` function on a `ModuleApi` instance. They accept a return model, component +properties definition, and a dialog component type. The dialog component itself must extend `DialogContent<>` from +the module API in order to open correctly. + +The dialog component overrides `trySubmit()` and returns a promise for the return model, which is then passed back through +to the promise returned by `openDialog()`. + +The `DialogContent<>` component is supplied with supporting components at the react-sdk layer to make dialog handling +generic: all a module needs to do is supply the content that goes into the dialog. + +### Using standard UI elements + +The react-sdk provides a number of components for building Matrix clients as well as some supporting components to make +it easier to have standardized styles on things like text inputs. Modules are naturally interested in these components +so their UI looks nearly indistinguishable from the rest of the app, however the react-sdk's components are not able to +be accessed directly. + +Instead, similar to dialogs and translations, modules use a proxy component which gets replaced by the real thing at +runtime. For example, there is a `TextInputField` component supplied by the module API which gets translated into a +decorated field at runtime for the module. + +**Note for react-sdk maintainers:** Don't forget to set the `renderFactory` of these components, otherwise the UI will +be subpar. + +### Account management + +Modules can register for an account without overriding the logged-in user's credentials with the `registerAccount()` +function on a `ModuleApi` instance. If the module would like to use those credentials, or has a different set of +credentials in mind, it can call `useAccount()` on a `ModuleApi` instance to overwrite (**without warning**) the current +user's credentials. + +### View management + +From the `RuntimeModule` instance, modules can listen to various events that happen within the client to override +a small bit of the UI behaviour. For example, listening for `RoomViewLifecycle.PreviewRoomNotLoggedIn` allows the module +to change the behaviour of the "room preview bar" to enable future cases of `RoomViewLifecycle.JoinFromRoomPreview` +being raised for additional handling. + +The module can also change which room the user is looking at, and join it, with `switchToRoom()` on a `ModuleApi` +instance. + +## Contributing / developing + +Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for the mechanics of the contribution process. + +For development, it is recommended to set up a normal element-web development environment and `yarn link` the +module API into both the react-sdk and element-web layers. + +Visit [#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) for support with getting a development +environment going. + +## Releases + +Because this is a scoped package, it needs to be published in a special way: + +```bash +npm publish --access public +``` diff --git a/src/ModuleApi.ts b/src/ModuleApi.ts index d1c5c2f..b52d9c2 100644 --- a/src/ModuleApi.ts +++ b/src/ModuleApi.ts @@ -19,6 +19,13 @@ import { DialogProps } from "./components/DialogContent"; import { AccountCredentials } from "./types/credentials"; import React from "react"; +/** + * A module API surface for the react-sdk. Provides a stable API for modules to + * interact with the internals of the react-sdk without having to update themselves + * for refactorings or code changes within the react-sdk. + * + * An instance of a ModuleApi is provided to all modules at runtime. + */ export interface ModuleApi { /** * Register strings with the translation engine. This supports overriding strings which @@ -40,14 +47,21 @@ export interface ModuleApi { * Opens a dialog in the client. * @param title The title of the dialog * @param body The function which creates a body component for the dialog. + * @param props Optional props to provide to the dialog. * @returns Whether the user submitted the dialog or closed it, and the model returned by the * dialog component if submitted. */ - // TODO: @@ Support input props to DialogContent - openDialog(title: string, body: (props: P, ref: React.RefObject) => React.ReactNode): Promise<{ didSubmit: boolean, model: M }>; + openDialog( + title: string, + body: (props: P, ref: React.RefObject) => React.ReactNode, + props?: Omit, + ): Promise<{ didSubmit: boolean, model: M }>; /** - * Registers for an account on the currently connected homeserver. + * Registers for an account on the currently connected homeserver. This requires that the homeserver + * offer a password-only flow without other flows. This means it is not traditionally compatible with + * homeservers like matrix.org which also generally require a combination of reCAPTCHA, email address, + * terms of service acceptance, etc. * @param username The username to register. * @param password The password to register. * @param displayName Optional display name to set. diff --git a/src/RuntimeModule.ts b/src/RuntimeModule.ts index 08e6c6f..db0601c 100644 --- a/src/RuntimeModule.ts +++ b/src/RuntimeModule.ts @@ -17,7 +17,7 @@ limitations under the License. import { EventEmitter } from "events"; import { ModuleApi } from "./ModuleApi"; -// TODO: @@ Type the event emitter with AnyLifecycle +// TODO: Type the event emitter with AnyLifecycle (extract TypedEventEmitter from js-sdk somehow?) /** * Represents a module which is loaded at runtime. Modules which implement this class diff --git a/src/types/credentials.ts b/src/types/credentials.ts index 798e7f0..6592496 100644 --- a/src/types/credentials.ts +++ b/src/types/credentials.ts @@ -14,9 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ +/** + * Matrix account credentials for a known user. + */ export interface AccountCredentials { + /** + * The user ID. + */ userId: string; + /** + * The device ID. + */ deviceId: string; + /** + * The access token belonging to this device ID and user ID. + */ accessToken: string; + /** + * The homeserver URL where the credentials are valid. + */ homeserverUrl: string; } diff --git a/src/types/translations.ts b/src/types/translations.ts index 925b0ea..b63fabf 100644 --- a/src/types/translations.ts +++ b/src/types/translations.ts @@ -14,6 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +/** + * Translations object mapping an input string to language variants. + * Mirrors custom translations support introduced by the react-sdk + * here: https://github.com/matrix-org/matrix-react-sdk/pull/7886 + */ export type TranslationStringsObject = { [str: string]: { [lang: string]: string; From e294adf677d387bb5bcc547cc01ceff974648721 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 12 May 2022 17:08:44 -0600 Subject: [PATCH 4/7] Appease the linter --- src/ModuleApi.ts | 3 ++- src/RuntimeModule.ts | 1 + test/placeholder.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 test/placeholder.test.ts diff --git a/src/ModuleApi.ts b/src/ModuleApi.ts index b52d9c2..47de8fa 100644 --- a/src/ModuleApi.ts +++ b/src/ModuleApi.ts @@ -14,10 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; + import { TranslationStringsObject } from "./types/translations"; import { DialogProps } from "./components/DialogContent"; import { AccountCredentials } from "./types/credentials"; -import React from "react"; /** * A module API surface for the react-sdk. Provides a stable API for modules to diff --git a/src/RuntimeModule.ts b/src/RuntimeModule.ts index db0601c..eab766a 100644 --- a/src/RuntimeModule.ts +++ b/src/RuntimeModule.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { EventEmitter } from "events"; + import { ModuleApi } from "./ModuleApi"; // TODO: Type the event emitter with AnyLifecycle (extract TypedEventEmitter from js-sdk somehow?) diff --git a/test/placeholder.test.ts b/test/placeholder.test.ts new file mode 100644 index 0000000..b19edf4 --- /dev/null +++ b/test/placeholder.test.ts @@ -0,0 +1,24 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +// We don't have anything which needs tests at the moment, so stub out a test +// to appease the linter's requirements. + +describe("placeholder", () => { + it('should pass the linter', () => { + // nothing to do. + }); +}); From 15b71d49b4cdad7da432524e287723cd1e3310f8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 12 May 2022 17:12:21 -0600 Subject: [PATCH 5/7] xref issue --- src/RuntimeModule.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/RuntimeModule.ts b/src/RuntimeModule.ts index eab766a..0e00d86 100644 --- a/src/RuntimeModule.ts +++ b/src/RuntimeModule.ts @@ -19,6 +19,7 @@ import { EventEmitter } from "events"; import { ModuleApi } from "./ModuleApi"; // TODO: Type the event emitter with AnyLifecycle (extract TypedEventEmitter from js-sdk somehow?) +// See https://github.com/matrix-org/matrix-react-sdk-module-api/issues/4 /** * Represents a module which is loaded at runtime. Modules which implement this class From cf43dff98a45d52eeb72b5da948f8b2758959f51 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 24 May 2022 21:13:46 -0600 Subject: [PATCH 6/7] Update surface for review --- src/ModuleApi.ts | 28 +++++++++++-------- src/RuntimeModule.ts | 3 +- src/components/DialogContent.tsx | 3 +- .../{credentials.ts => AccountAuthInfo.ts} | 4 +-- src/types/translations.ts | 5 ++++ 5 files changed, 27 insertions(+), 16 deletions(-) rename src/types/{credentials.ts => AccountAuthInfo.ts} (90%) diff --git a/src/ModuleApi.ts b/src/ModuleApi.ts index 47de8fa..0a0b857 100644 --- a/src/ModuleApi.ts +++ b/src/ModuleApi.ts @@ -16,9 +16,9 @@ limitations under the License. import React from "react"; -import { TranslationStringsObject } from "./types/translations"; +import { PlainSubstitution, TranslationStringsObject } from "./types/translations"; import { DialogProps } from "./components/DialogContent"; -import { AccountCredentials } from "./types/credentials"; +import { AccountAuthInfo } from "./types/AccountAuthInfo"; /** * A module API surface for the react-sdk. Provides a stable API for modules to @@ -42,7 +42,7 @@ export interface ModuleApi { * @param variables The variables to replace, if any. * @returns The translated string. */ - translateString(s: string, variables?: Record): string; + translateString(s: string, variables?: Record): string; /** * Opens a dialog in the client. @@ -56,7 +56,7 @@ export interface ModuleApi { title: string, body: (props: P, ref: React.RefObject) => React.ReactNode, props?: Omit, - ): Promise<{ didSubmit: boolean, model: M }>; + ): Promise<{ didOkOrSubmit: boolean, model: M }>; /** * Registers for an account on the currently connected homeserver. This requires that the homeserver @@ -66,23 +66,27 @@ export interface ModuleApi { * @param username The username to register. * @param password The password to register. * @param displayName Optional display name to set. - * @returns Resolves to the credentials for the created account. + * @returns Resolves to the authentication info for the created account. */ - registerAccount(username: string, password: string, displayName?: string): Promise; + registerSimpleAccount(username: string, password: string, displayName?: string): Promise; /** * Switches the user's currently logged-in account to the one specified. The user will not * be warned. - * @param credentials The credentials to log in with. + * @param accountAuthInfo The authentication info to log in with. * @returns Resolves when complete. */ - useAccount(credentials: AccountCredentials): Promise; + overwriteAccountAuth(accountAuthInfo: AccountAuthInfo): Promise; /** - * Switches the user's current view to look at the given room, joining it if required. - * @param roomId The room ID to look at. - * @param andJoin True to also join the room if needed. + * Switches the user's current view to look at the given permalink. If the permalink is + * a room, it can optionally be joined automatically if required. + * + * Permalink must be a matrix.to permalink at this time. + * @param uri The URI to navigate to. + * @param andJoin True to also join the room if needed. Does nothing if the link isn't to + * a room. * @returns Resolves when complete. */ - switchToRoom(roomId: string, andJoin?: boolean): Promise; + navigatePermalink(uri: string, andJoin?: boolean): Promise; } diff --git a/src/RuntimeModule.ts b/src/RuntimeModule.ts index 0e00d86..704561f 100644 --- a/src/RuntimeModule.ts +++ b/src/RuntimeModule.ts @@ -17,6 +17,7 @@ limitations under the License. import { EventEmitter } from "events"; import { ModuleApi } from "./ModuleApi"; +import { PlainSubstitution } from "./types/translations"; // TODO: Type the event emitter with AnyLifecycle (extract TypedEventEmitter from js-sdk somehow?) // See https://github.com/matrix-org/matrix-react-sdk-module-api/issues/4 @@ -37,7 +38,7 @@ export abstract class RuntimeModule extends EventEmitter { * @returns The translated string. * @protected */ - protected t(s: string, variables?: Record): string { + protected t(s: string, variables?: Record): string { return this.moduleApi.translateString(s, variables); } } diff --git a/src/components/DialogContent.tsx b/src/components/DialogContent.tsx index 6b03979..cf3152e 100644 --- a/src/components/DialogContent.tsx +++ b/src/components/DialogContent.tsx @@ -16,6 +16,7 @@ limitations under the License. import * as React from "react"; import { ModuleApi } from "../ModuleApi"; +import { PlainSubstitution } from "../types/translations"; export interface DialogProps { moduleApi: ModuleApi; @@ -45,7 +46,7 @@ export abstract class DialogContent

): string { + protected t(s: string, variables?: Record): string { return this.props.moduleApi.translateString(s, variables); } diff --git a/src/types/credentials.ts b/src/types/AccountAuthInfo.ts similarity index 90% rename from src/types/credentials.ts rename to src/types/AccountAuthInfo.ts index 6592496..9e68f1e 100644 --- a/src/types/credentials.ts +++ b/src/types/AccountAuthInfo.ts @@ -15,9 +15,9 @@ limitations under the License. */ /** - * Matrix account credentials for a known user. + * Matrix account authentication information for a known user. */ -export interface AccountCredentials { +export interface AccountAuthInfo { /** * The user ID. */ diff --git a/src/types/translations.ts b/src/types/translations.ts index b63fabf..bb41ccd 100644 --- a/src/types/translations.ts +++ b/src/types/translations.ts @@ -24,3 +24,8 @@ export type TranslationStringsObject = { [lang: string]: string; }; }; + +/** + * Represents a simple translation replacement (non-component replacement) + */ +export type PlainSubstitution = number | string; From 3f60091d9eb83a37e25f9839549ee0ab513fa142 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 24 May 2022 21:19:05 -0600 Subject: [PATCH 7/7] Update readme for interface changes --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c15c3a0..cfc32bb 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,10 @@ be subpar. ### Account management -Modules can register for an account without overriding the logged-in user's credentials with the `registerAccount()` -function on a `ModuleApi` instance. If the module would like to use those credentials, or has a different set of -credentials in mind, it can call `useAccount()` on a `ModuleApi` instance to overwrite (**without warning**) the current -user's credentials. +Modules can register for an account without overriding the logged-in user's auth data with the `registerSimpleAccount()` +function on a `ModuleApi` instance. If the module would like to use that auth data, or has a different set of +authentication information in mind, it can call `overwriteAccountAuth()` on a `ModuleApi` instance to overwrite +(**without warning**) the current user's session. ### View management @@ -95,8 +95,8 @@ a small bit of the UI behaviour. For example, listening for `RoomViewLifecycle.P to change the behaviour of the "room preview bar" to enable future cases of `RoomViewLifecycle.JoinFromRoomPreview` being raised for additional handling. -The module can also change which room the user is looking at, and join it, with `switchToRoom()` on a `ModuleApi` -instance. +The module can also change what room/user/entity the user is looking at, and join it (if it's a room), with +`navigatePermalink` on a `ModuleApi` instance. ## Contributing / developing