This repository has been archived by the owner on Jan 30, 2025. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 9
Initial surface for module API (enough for an ILAG module) #1
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
de97ef7
Early structures for a module API surface
turt2live 7446412
Add public interface for what an ILAG module would need
turt2live f47cdd4
Add some docs
turt2live e294adf
Appease the linter
turt2live 15b71d4
xref issue
turt2live cf43dff
Update surface for review
turt2live 3f60091
Update readme for interface changes
turt2live File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 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 | ||
|
||
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 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 | ||
|
||
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* | ||
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 React from "react"; | ||
|
||
import { PlainSubstitution, TranslationStringsObject } from "./types/translations"; | ||
import { DialogProps } from "./components/DialogContent"; | ||
import { AccountAuthInfo } from "./types/AccountAuthInfo"; | ||
|
||
/** | ||
* 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 | ||
* 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, PlainSubstitution>): 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. | ||
* @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. | ||
*/ | ||
openDialog<M extends object, P extends DialogProps = DialogProps, C extends React.Component = React.Component>( | ||
title: string, | ||
body: (props: P, ref: React.RefObject<C>) => React.ReactNode, | ||
props?: Omit<P, keyof DialogProps>, | ||
): Promise<{ didOkOrSubmit: boolean, model: M }>; | ||
|
||
/** | ||
* 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. | ||
* @returns Resolves to the authentication info for the created account. | ||
*/ | ||
registerSimpleAccount(username: string, password: string, displayName?: string): Promise<AccountAuthInfo>; | ||
|
||
/** | ||
* Switches the user's currently logged-in account to the one specified. The user will not | ||
* be warned. | ||
* @param accountAuthInfo The authentication info to log in with. | ||
* @returns Resolves when complete. | ||
*/ | ||
overwriteAccountAuth(accountAuthInfo: AccountAuthInfo): Promise<void>; | ||
|
||
/** | ||
* 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. | ||
*/ | ||
navigatePermalink(uri: string, andJoin?: boolean): Promise<void>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
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"; | ||
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 | ||
|
||
/** | ||
* 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, PlainSubstitution>): string { | ||
return this.moduleApi.translateString(s, variables); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/* | ||
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"; | ||
import { PlainSubstitution } from "../types/translations"; | ||
|
||
export interface DialogProps { | ||
moduleApi: ModuleApi; | ||
} | ||
|
||
export interface DialogState { | ||
busy: boolean; | ||
error?: string; | ||
} | ||
|
||
export abstract class DialogContent<P extends DialogProps = DialogProps, S extends DialogState = DialogState, M extends object = {}> | ||
extends React.PureComponent<P, S> { | ||
|
||
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, PlainSubstitution>): 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<M>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't this just grow a peer-dep to js-sdk given it knows it'll have a js-sdk from react-sdk?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially. I've been trying to avoid the peer dependency style as it'll end up being a slew of warnings for developers (because we won't update the dependency very often)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's fine though because its a peer-dep, you could even specify the min version and say any version from there.
>=12.4.1
(replacing with the actual version we introduced TypedEventEmitter)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
until we release a breaking change of the js-sdk and then it is considered unmatched (both logically and by the semver checks - we can't assume it'll continue to compile).
For now I'm not that concerned with typed event emitter support. Documentation can supplement it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then why not add yet another layer called
matrix-js-common
or something :P?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure if serious, but it's quite literally on the table for consideration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be a good home for some things for element-desktop to re-use too, like
fetchdep.sh
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably for a larger conversation outside of this PR though ;)