feat: mini apps customizados com importacao por arquivo/URL, paleta de cores, avatar autenticado e documentacao
This commit is contained in:
178
docs/miniapps.md
Normal file
178
docs/miniapps.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user