feat: mini apps customizados com importacao por arquivo/URL, paleta de cores, avatar autenticado e documentacao
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

This commit is contained in:
2026-09-18 15:11:12 -03:00
parent 94632e0611
commit 1f692b1e4e
18 changed files with 2186 additions and 2 deletions

178
docs/miniapps.md Normal file
View 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`.