178 lines
6.3 KiB
Markdown
178 lines
6.3 KiB
Markdown
# TatuChat Mini Apps
|
||
|
||
Mini apps are small, self-contained HTML documents that run inside TatuChat in a
|
||
sandboxed web view. They can read/write their own key-value store, inspect the
|
||
user's profile and read/write arbitrary account data through a tiny messaging
|
||
bridge. There are no dependencies and no build step: an app is just one `.html`
|
||
file.
|
||
|
||
## Creating a mini app
|
||
|
||
1. Write a single `.html` file (assets may be inlined; see limitations below).
|
||
2. Import it in TatuChat:
|
||
- **From file:** open *Apps* → the **import** menu → *Import from file* and
|
||
pick your `.html` file.
|
||
- **From URL:** *Apps* → import menu → *Import from URL* and paste a link to
|
||
the HTML document.
|
||
3. Open the app from the *Installed* list. Use the reload button in the top bar
|
||
while developing.
|
||
|
||
The available apps page (`/rooms/apps`) lists the built-in apps (`tasks`,
|
||
`accountdata`) under *Installed* once you install them, plus every app you
|
||
import.
|
||
|
||
## Talking to TatuChat
|
||
|
||
The host listens for `message` events. A channel app sends a request with
|
||
`window.parent.postMessage`, and the host answers with the same `requestId`.
|
||
Replies are delivered in an event with a `response` object of the form
|
||
`{data, error}`.
|
||
|
||
Request payload:
|
||
|
||
```js
|
||
{
|
||
api: 'fromMiniApp',
|
||
widgetId: '<appId>',
|
||
requestId: 'any-unique-string',
|
||
action: 'io.tatuchat.storage.load', // one of the actions below
|
||
data: { /* action-specific payload */ }
|
||
}
|
||
```
|
||
|
||
Helper for your app (copy this into your HTML):
|
||
|
||
```js
|
||
const APP_ID = 'myapp'; // must match the app id
|
||
const _pending = new Map();
|
||
let _requestCounter = 0;
|
||
|
||
function bridgeRequest(action, data) {
|
||
return new Promise(function (resolve, reject) {
|
||
const requestId = 'r' + (++_requestCounter);
|
||
_pending.set(requestId, { resolve: resolve, reject: reject });
|
||
window.parent.postMessage({
|
||
api: 'fromMiniApp',
|
||
widgetId: APP_ID,
|
||
requestId: requestId,
|
||
action: action,
|
||
data: data
|
||
}, '*');
|
||
});
|
||
}
|
||
|
||
window.addEventListener('message', function (event) {
|
||
const msg = event.data;
|
||
if (!msg || msg.api !== 'fromMiniApp' || !msg.requestId) return;
|
||
if (!_pending.has(msg.requestId)) return;
|
||
const pending = _pending.get(msg.requestId);
|
||
_pending.delete(msg.requestId);
|
||
const response = msg.response || {};
|
||
if (response.error) pending.reject(new Error(response.error));
|
||
else pending.resolve(response.data);
|
||
});
|
||
```
|
||
|
||
## Available actions
|
||
|
||
### `io.tatuchat.storage.load`
|
||
|
||
Loads the app's own key-value store (persisted in the user's account data under
|
||
`im.tatuchat.miniapps.data.<appId>`). Resolves with a plain object.
|
||
|
||
```js
|
||
const data = await bridgeRequest('io.tatuchat.storage.load');
|
||
// data => { ... } (any JSON values previously saved)
|
||
```
|
||
|
||
### `io.tatuchat.storage.save`
|
||
|
||
Replaces the whole store. `data` must be an object. Resolves with `null`.
|
||
|
||
```js
|
||
await bridgeRequest('io.tatuchat.storage.save', { tasks: [...], version: 1 });
|
||
```
|
||
|
||
> Note: this **replaces** the entire store; there is no per-key merge. Load,
|
||
> mutate, then save.
|
||
|
||
### `io.tatuchat.identity.get`
|
||
|
||
Resolves with the signed-in user's profile:
|
||
|
||
```js
|
||
const info = await bridgeRequest('io.tatuchat.identity.get');
|
||
// => {
|
||
// userId: '@user:server',
|
||
// displayName: 'Tatu',
|
||
// avatarUrl: 'mxc://server/...',
|
||
// avatarHttpUrl: 'https://.../download/...', // may require auth
|
||
// avatarDataUri: 'data:image/png;base64,...', // always renderable
|
||
// homeserver: 'https://server.example'
|
||
// }
|
||
```
|
||
|
||
The `avatarDataUri` is a base64 data URI fetched by TatuChat with the user's
|
||
credentials. **Use `avatarDataUri` for `<img src>`** – the homeserver usually
|
||
requires an authenticated media request that a plain `<img>` tag cannot send
|
||
(this is what caused the `M_MISSING_TOKEN` error). Fall back to `avatarHttpUrl`
|
||
only if `avatarDataUri` is absent.
|
||
|
||
### `io.tatuchat.theme.get`
|
||
|
||
Resolves with the color palette currently used by TatuChat, so your app can
|
||
match the ecosystem (light/dark aware). Colors are `#AARRGGBB` hex strings
|
||
suitable for CSS.
|
||
|
||
```js
|
||
const { palette } = await bridgeRequest('io.tatuchat.theme.get');
|
||
// => {
|
||
// brightness: 'light' | 'dark',
|
||
// primary: '#FF00D084',
|
||
// onPrimary: '#FF000000',
|
||
// primaryContainer: '#FF...',
|
||
// surface: '#FF...',
|
||
// surfaceContainerLowest/Low/Container/High/Highest: '#FF...',
|
||
// secondary..., tertiary..., error...,
|
||
// outline, outlineVariant, background, onBackground, onSurface, ...
|
||
// }
|
||
document.documentElement.style.setProperty('--primary', palette.primary);
|
||
```
|
||
|
||
### `io.tatuchat.accountdata.list`
|
||
|
||
Resolves with a map of **all** account data of the signed-in user as an object
|
||
of `{ key: { ...content } }`.
|
||
|
||
### `io.tatuchat.accountdata.get`
|
||
|
||
`data: { key: 'org.example.something' }` resolves with `{ key, content }` where
|
||
`content` is the stored JSON content (or `{}` if absent).
|
||
|
||
### `io.tatuchat.accountdata.set`
|
||
|
||
`data: { key: 'org.example.something', content: { ... } }` stores or replaces
|
||
the content of that account data key. Resolves with `null`.
|
||
|
||
## Caveats & limitations
|
||
|
||
- **Distribution / installation:** apps imported from URL must be served with
|
||
permissive CORS headers (`Access-Control-Allow-Origin`) so the browser can
|
||
read them. On the web, TatuChat retries through a CORS proxy if you configure
|
||
one (setting `miniAppCorsProxy` in the app settings/config). If the server
|
||
blocks cross-origin reads, import the `.html` file manually instead.
|
||
- **Size:** the HTML source is stored in the user's account data, where
|
||
individual events are limited to about **64 KB** (homeservers like matrix.org
|
||
enforce this). Keep mini apps small and inline the assets; or load heavy
|
||
content from a URL at runtime.
|
||
- **Security:** the app runs in a sandboxed web view with `allow-scripts` /
|
||
`allow-forms` / `allow-same-origin`. It can only reach TatuChat through the
|
||
bridge actions above – it cannot access the user's rooms, messages or files.
|
||
- **No unsolicited access:** data is only exchanged when the app explicitly
|
||
calls the bridge. The host never pushes data into the app on its own.
|
||
- **Store is per app:** every app has its own `storage.load`/`save` namespace.
|
||
`accountdata.*` is the only way to share data between apps.
|
||
- **`storage.save` overwrites:** there is no merge/patch semantics.
|
||
- **Native platforms:** the HTML is rendered by the platform web view; take
|
||
care with features that differ between browsers (e.g. storage APIs inside the
|
||
sandbox, `window.parent` assumptions). Prefer the bridge over `localStorage`. |