Files
tatuchat/docs/miniapps.md
Welton Moura 6c689d1f34
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
feat: tela de apps com logos/menu de exclusao, correcao de tema nas mini apps e sidebar de navegacao
2026-09-19 00:08:35 -03:00

209 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
## Manifest (meta tags)
Before installing an app, TatuChat reads the `<meta>` tags of the document and
uses them as the app's manifest. **Every tag listed below must be present** in
the `<head>` of your document, even if the value is empty — except `id` and
`name`, which must also be non-empty. An app that is missing any of these tags
**cannot be installed**.
```html
<meta name="mini-app-id" content="myapp">
<meta name="mini-app-name" content="My App">
<meta name="mini-app-category" content="productivity">
<meta name="mini-app-description" content="What my app does">
<meta name="mini-app-logo" content=""> <!-- empty is allowed -->
<meta name="mini-app-in-chat" content="false"> <!-- true|false -->
```
`mini-app-in-chat` must be `true` or `false` (empty defaults to `false`).
Values are trimmed and read case-insensitively.
The `mini-app-id` is used **as the key in the account data store**: the app's
store is persisted under `im.tatuchat.miniapps.data.<id>` and its HTML source
under `im.tatuchat.miniapps.source.<id>`. It must be unique: ids of built-in
apps (`tasks`, `accountdata`) are reserved and an already installed id cannot
be reused. This id is also the `widgetId` your app must send in the bridge
protocol.
The remaining fields are stored as metadata (`category`, `description`, `logo`,
`inChat`) and shown in the *Apps* page.
## 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 (msg.response === undefined) 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 `#RRGGBBAA` hex strings
(CSS 8-digit order: red, green, blue, alpha) suitable for CSS.
```js
const { palette } = await bridgeRequest('io.tatuchat.theme.get');
// => {
// brightness: 'light' | 'dark',
// primary: '#00D084FF',
// onPrimary: '#000000FF',
// primaryContainer: '#00D08433',
// surface: '#FEF7FF00',
// surfaceContainerLowest/Low/Container/High/Highest: '#…',
// 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`.