Skip to content
This repository was archived by the owner on Jan 30, 2025. It is now read-only.

Commit 7a78f98

Browse files
authored
Initial surface for module API (enough for an ILAG module) (#1)
* Early structures for a module API surface * Add public interface for what an ILAG module would need * Add some docs * Appease the linter * xref issue * Update surface for review * Update readme for interface changes
1 parent 170a745 commit 7a78f98

16 files changed

+635
-12
lines changed

.babelrc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
"sourceMaps": true,
33
"presets": [
44
"@babel/preset-env",
5+
"@babel/preset-react",
56
"@babel/preset-typescript"
67
],
78
"plugins": [
8-
"@babel/plugin-proposal-class-properties"
9+
"@babel/plugin-proposal-class-properties",
10+
"@babel/plugin-transform-runtime"
911
]
1012
}

README.md

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,117 @@
11
# matrix-react-sdk-module-api
2-
Proof of concept API surface for writing Modules for the react-sdk
32

4-
## TODO
3+
API surface for interacting with the [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk) in a safe
4+
and predictable way.
55

6-
* [ ] Write a better intro/readme
7-
* [ ] Proof of concept
8-
* [ ] If approved, make it a real npm package
9-
* [ ] If approved, fix access controls
10-
* [ ] If approved, maintain this
6+
Modules are simply additional functionality added at compile time for the application and can do things like register
7+
custom translations, translation overrides, open dialogs, and add/modify UI.
8+
9+
**Note**: This project is still considered alpha/beta quality due to the API surface not being extensive. Please reach
10+
out in [#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) on Matrix for guidance on how to add to
11+
this API surface.
12+
13+
In general, new code should target a generalized interface. An example would be the `openDialog()` function: while the
14+
first module to use it didn't need custom `props`, it is expected that a dialog would at some point, so we expose it.
15+
On the other hand, we deliberately do not expose the complexity of the react-sdk's dialog stack to this layer until
16+
we need it. We might choose to open sticky dialogs with a new `openStickyDialog()` function instead of appending more
17+
arguments to the existing function.
18+
19+
## Using the API
20+
21+
Modules are simply standalone npm packages which get installed/included in the app at compile time. To start, we
22+
recommend using a simple module as a template, such as [element-web-ilag-module](https://github.com/vector-im/element-web-ilag-module).
23+
24+
The package's `main` entrypoint MUST point to an instance of `RuntimeModule`. That class must be a `default` export
25+
for the module loader to reference correctly.
26+
27+
The `RuntimeModule` instance MUST have a constructor which accepts a single `ModuleApi` parameter. This is supplied
28+
to the `super()` constructor.
29+
30+
Otherwise, simply `npm install --save @matrix-org/react-sdk-module-api` and start coding!
31+
32+
### Custom translations / string overrides
33+
34+
Custom translation strings (used within your module) or string overrides can be specified using the `registerTranslations`
35+
function on a `ModuleApi` instance. For example:
36+
37+
```typescript
38+
this.moduleApi.registerTranslations({
39+
// If you use the translation utilities within your module, register your strings
40+
"My custom string": {
41+
"en": "My custom string",
42+
"fr": "Ma chaîne personnalisée",
43+
},
44+
45+
// If you want to override a string already in the app, such as the power level role
46+
// names, use the base string here and redefine the values for each applicable language.
47+
"A string that might already exist in the app": {
48+
"en": "Replacement value for that string",
49+
"fr": "Valeur de remplacement pour cette chaîne",
50+
},
51+
});
52+
```
53+
54+
If you are within a class provided by the module API then translations are generally accessible with `this.t("my string")`.
55+
This is a shortcut to `this.moduleApi.translateString()` which in turn calls into the translation engine at runtime to
56+
determine which appropriately-translated string should be returned.
57+
58+
### Opening dialogs
59+
60+
Dialogs are opened through the `openDialog()` function on a `ModuleApi` instance. They accept a return model, component
61+
properties definition, and a dialog component type. The dialog component itself must extend `DialogContent<>` from
62+
the module API in order to open correctly.
63+
64+
The dialog component overrides `trySubmit()` and returns a promise for the return model, which is then passed back through
65+
to the promise returned by `openDialog()`.
66+
67+
The `DialogContent<>` component is supplied with supporting components at the react-sdk layer to make dialog handling
68+
generic: all a module needs to do is supply the content that goes into the dialog.
69+
70+
### Using standard UI elements
71+
72+
The react-sdk provides a number of components for building Matrix clients as well as some supporting components to make
73+
it easier to have standardized styles on things like text inputs. Modules are naturally interested in these components
74+
so their UI looks nearly indistinguishable from the rest of the app, however the react-sdk's components are not able to
75+
be accessed directly.
76+
77+
Instead, similar to dialogs and translations, modules use a proxy component which gets replaced by the real thing at
78+
runtime. For example, there is a `TextInputField` component supplied by the module API which gets translated into a
79+
decorated field at runtime for the module.
80+
81+
**Note for react-sdk maintainers:** Don't forget to set the `renderFactory` of these components, otherwise the UI will
82+
be subpar.
83+
84+
### Account management
85+
86+
Modules can register for an account without overriding the logged-in user's auth data with the `registerSimpleAccount()`
87+
function on a `ModuleApi` instance. If the module would like to use that auth data, or has a different set of
88+
authentication information in mind, it can call `overwriteAccountAuth()` on a `ModuleApi` instance to overwrite
89+
(**without warning**) the current user's session.
90+
91+
### View management
92+
93+
From the `RuntimeModule` instance, modules can listen to various events that happen within the client to override
94+
a small bit of the UI behaviour. For example, listening for `RoomViewLifecycle.PreviewRoomNotLoggedIn` allows the module
95+
to change the behaviour of the "room preview bar" to enable future cases of `RoomViewLifecycle.JoinFromRoomPreview`
96+
being raised for additional handling.
97+
98+
The module can also change what room/user/entity the user is looking at, and join it (if it's a room), with
99+
`navigatePermalink` on a `ModuleApi` instance.
100+
101+
## Contributing / developing
102+
103+
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for the mechanics of the contribution process.
104+
105+
For development, it is recommended to set up a normal element-web development environment and `yarn link` the
106+
module API into both the react-sdk and element-web layers.
107+
108+
Visit [#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) for support with getting a development
109+
environment going.
110+
111+
## Releases
112+
113+
Because this is a scoped package, it needs to be published in a special way:
114+
115+
```bash
116+
npm publish --access public
117+
```

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"clean": "rimraf lib",
1919
"build": "yarn clean && yarn build:compile && yarn build:types",
2020
"build:types": "tsc -p ./tsconfig.build.json --emitDeclarationOnly",
21-
"build:compile": "babel -d lib --verbose --extensions \".ts\" src",
21+
"build:compile": "babel -d lib --verbose --extensions \".ts,.tsx\" src",
2222
"start": "tsc -p ./tsconfig.build.json -w",
2323
"test": "jest",
2424
"lint": "eslint src test && tsc --noEmit",
@@ -30,9 +30,12 @@
3030
"@babel/eslint-parser": "^7.17.0",
3131
"@babel/eslint-plugin": "^7.17.7",
3232
"@babel/plugin-proposal-class-properties": "^7.16.7",
33+
"@babel/plugin-transform-runtime": "^7.17.0",
3334
"@babel/preset-env": "^7.16.11",
35+
"@babel/preset-react": "^7.16.7",
3436
"@babel/preset-typescript": "^7.16.7",
3537
"@types/jest": "^27.4.1",
38+
"@types/react": "^17",
3639
"@typescript-eslint/eslint-plugin": "^5.18.0",
3740
"@typescript-eslint/parser": "^5.18.0",
3841
"eslint": "^8.12.0",
@@ -43,5 +46,8 @@
4346
"rimraf": "^3.0.2",
4447
"ts-jest": "^27.1.4",
4548
"typescript": "^4.6.3"
49+
},
50+
"dependencies": {
51+
"@babel/runtime": "^7.17.9"
4652
}
4753
}

src/ModuleApi.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from "react";
18+
19+
import { PlainSubstitution, TranslationStringsObject } from "./types/translations";
20+
import { DialogProps } from "./components/DialogContent";
21+
import { AccountAuthInfo } from "./types/AccountAuthInfo";
22+
23+
/**
24+
* A module API surface for the react-sdk. Provides a stable API for modules to
25+
* interact with the internals of the react-sdk without having to update themselves
26+
* for refactorings or code changes within the react-sdk.
27+
*
28+
* An instance of a ModuleApi is provided to all modules at runtime.
29+
*/
30+
export interface ModuleApi {
31+
/**
32+
* Register strings with the translation engine. This supports overriding strings which
33+
* the system is already aware of.
34+
* @param translations The translations to load.
35+
*/
36+
registerTranslations(translations: TranslationStringsObject): void;
37+
38+
/**
39+
* Runs a string through the translation engine. If variables are needed, use %(varName)s
40+
* as a placeholder for varName in the variables object.
41+
* @param s The string. Should already be known to the engine.
42+
* @param variables The variables to replace, if any.
43+
* @returns The translated string.
44+
*/
45+
translateString(s: string, variables?: Record<string, PlainSubstitution>): string;
46+
47+
/**
48+
* Opens a dialog in the client.
49+
* @param title The title of the dialog
50+
* @param body The function which creates a body component for the dialog.
51+
* @param props Optional props to provide to the dialog.
52+
* @returns Whether the user submitted the dialog or closed it, and the model returned by the
53+
* dialog component if submitted.
54+
*/
55+
openDialog<M extends object, P extends DialogProps = DialogProps, C extends React.Component = React.Component>(
56+
title: string,
57+
body: (props: P, ref: React.RefObject<C>) => React.ReactNode,
58+
props?: Omit<P, keyof DialogProps>,
59+
): Promise<{ didOkOrSubmit: boolean, model: M }>;
60+
61+
/**
62+
* Registers for an account on the currently connected homeserver. This requires that the homeserver
63+
* offer a password-only flow without other flows. This means it is not traditionally compatible with
64+
* homeservers like matrix.org which also generally require a combination of reCAPTCHA, email address,
65+
* terms of service acceptance, etc.
66+
* @param username The username to register.
67+
* @param password The password to register.
68+
* @param displayName Optional display name to set.
69+
* @returns Resolves to the authentication info for the created account.
70+
*/
71+
registerSimpleAccount(username: string, password: string, displayName?: string): Promise<AccountAuthInfo>;
72+
73+
/**
74+
* Switches the user's currently logged-in account to the one specified. The user will not
75+
* be warned.
76+
* @param accountAuthInfo The authentication info to log in with.
77+
* @returns Resolves when complete.
78+
*/
79+
overwriteAccountAuth(accountAuthInfo: AccountAuthInfo): Promise<void>;
80+
81+
/**
82+
* Switches the user's current view to look at the given permalink. If the permalink is
83+
* a room, it can optionally be joined automatically if required.
84+
*
85+
* Permalink must be a matrix.to permalink at this time.
86+
* @param uri The URI to navigate to.
87+
* @param andJoin True to also join the room if needed. Does nothing if the link isn't to
88+
* a room.
89+
* @returns Resolves when complete.
90+
*/
91+
navigatePermalink(uri: string, andJoin?: boolean): Promise<void>;
92+
}

src/RuntimeModule.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { EventEmitter } from "events";
18+
19+
import { ModuleApi } from "./ModuleApi";
20+
import { PlainSubstitution } from "./types/translations";
21+
22+
// TODO: Type the event emitter with AnyLifecycle (extract TypedEventEmitter from js-sdk somehow?)
23+
// See https://github.com/matrix-org/matrix-react-sdk-module-api/issues/4
24+
25+
/**
26+
* Represents a module which is loaded at runtime. Modules which implement this class
27+
* will be provided information about the application state and can react to it.
28+
*/
29+
export abstract class RuntimeModule extends EventEmitter {
30+
protected constructor(protected readonly moduleApi: ModuleApi) {
31+
super();
32+
}
33+
34+
/**
35+
* Run a string through the translation engine. Shortcut to ModuleApi#translateString().
36+
* @param s The string.
37+
* @param variables The variables, if any.
38+
* @returns The translated string.
39+
* @protected
40+
*/
41+
protected t(s: string, variables?: Record<string, PlainSubstitution>): string {
42+
return this.moduleApi.translateString(s, variables);
43+
}
44+
}

src/components/DialogContent.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import * as React from "react";
18+
import { ModuleApi } from "../ModuleApi";
19+
import { PlainSubstitution } from "../types/translations";
20+
21+
export interface DialogProps {
22+
moduleApi: ModuleApi;
23+
}
24+
25+
export interface DialogState {
26+
busy: boolean;
27+
error?: string;
28+
}
29+
30+
export abstract class DialogContent<P extends DialogProps = DialogProps, S extends DialogState = DialogState, M extends object = {}>
31+
extends React.PureComponent<P, S> {
32+
33+
protected constructor(props: P, state?: S) {
34+
super(props);
35+
36+
this.state = {
37+
busy: false,
38+
...state,
39+
};
40+
}
41+
42+
/**
43+
* Run a string through the translation engine. Shortcut to ModuleApi#translateString().
44+
* @param s The string.
45+
* @param variables The variables, if any.
46+
* @returns The translated string.
47+
* @protected
48+
*/
49+
protected t(s: string, variables?: Record<string, PlainSubstitution>): string {
50+
return this.props.moduleApi.translateString(s, variables);
51+
}
52+
53+
/**
54+
* Called when the dialog is submitted. Note that calling this will not submit the
55+
* dialog by default - this component will be wrapped in a form which handles keyboard
56+
* submission and buttons on its own.
57+
*
58+
* If the returned promise resolves then the dialog will be closed, otherwise the dialog
59+
* will stay open.
60+
*/
61+
public abstract trySubmit(): Promise<M>;
62+
}

src/components/Spinner.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import * as React from "react";
18+
19+
export class Spinner extends React.PureComponent {
20+
/**
21+
* The factory this component uses to render itself. Set to a different value to override.
22+
* @returns The component, rendered.
23+
*/
24+
public static renderFactory = (): React.ReactNode => null;
25+
26+
public render() {
27+
return Spinner.renderFactory();
28+
}
29+
}

0 commit comments

Comments
 (0)