Orbit documentation: docs, wiki and FAQ pages

This commit is contained in:
reverse 2026-06-20 06:34:12 +00:00
commit bcbe5fff22
No known key found for this signature in database
18 changed files with 714 additions and 0 deletions

40
docs/architecture.md Normal file
View file

@ -0,0 +1,40 @@
# Architecture
Orbit is a small, modular React app. No framework magic — a thin IRC layer feeds a zustand store,
which renders components.
## Project tree
```
src/
config.ts App.tsx main.tsx index.css
lib/ format.tsx editor.ts ← presentation + editor helpers
ui/ theme.ts prefs.ts viewport.ts
services/ notify.ts push.ts avatars.ts
irc/ parser client modes numerics casemap types caps ctcp
store.ts store/text.ts store/persistence.ts
components/
Chat.tsx ← thin assembler
chat/ MessageList Composer Sidebar Topbar MemberList Banners
settings/ SettingsModal
profile/ ProfileModal
modals/ Modals
Avatar ConnectScreen Turnstile
```
## Layers
- **`irc/`** — the protocol. `client.ts` is the WebSocket transport + CAP negotiation; `parser.ts`,
`modes.ts`, `casemap.ts`, `numerics.ts` are pure primitives. `caps.ts` lists the requested
capabilities.
- **`store.ts`** — turns IRC events into buffers, messages and members (zustand). Pure helpers split
into `store/text.ts` (masking, formatting) and `store/persistence.ts`.
- **`lib/`** — `format.tsx` renders mIRC formatting → React; `editor.ts` is the rich-composer
serialization.
- **`components/`** — the UI, grouped by area (`chat/`, `settings/`, `profile/`, `modals/`).
`Chat.tsx` is a ~30-line assembler.
## Quality
Vitest unit tests cover the pure modules (parser, casemap, masking). Lint + test + build run in CI,
and the self-hosted [push-to-deploy](/docs/push-to-deploy/) gates on the same.

51
docs/branding.md Normal file
View file

@ -0,0 +1,51 @@
# Branding & themes
Everything that ties Orbit to a particular network or brand lives in
[`config.json`](/docs/config/) under `branding` and `defaults` — no code changes, no rebuild.
## Rebrand
```json
{
"branding": {
"name": "ExampleChat",
"icon": "https://example.org/logo.svg",
"url": "https://example.org",
"tagline": "Chat with",
"taglineEm": "everyone.",
"subtitle": "Public rooms, private messages, no signup required."
}
}
```
This changes the connect screen, the console title, the network icon, and the CTCP VERSION/SOURCE
replies the client sends.
## Themes
Orbit ships four themes:
- **light** — clean, default.
- **dark** — easy on the eyes.
- **yomirc** — a retro mIRC / Windows-95 skin (silver chrome, fixed-width log lines).
- **yomirc-dark** — the retro skin in a dark palette.
Set the default for new users:
```json
{ "defaults": { "theme": "dark" } }
```
Users can switch any time in **Settings → Appearance**. See the [Themes wiki page](/wiki/themes/).
## Feature flags
Hide whole features per deployment:
```json
{ "features": { "push": true, "imageUpload": true, "register": false } }
```
- `push` — the Web Push notifications row.
- `imageUpload` — the composer image button + paste/drag upload.
- `register` — the "create an account" tab.

100
docs/compiled-plugins.md Normal file
View file

@ -0,0 +1,100 @@
# Compiled plugins
A quick `.js` plugin (see [Plugin system](/docs/plugins/)) is great for small
things. For anything substantial — real components with state and hooks — build
the plugin like a normal project and compile it down to **one droppable file**: a
compiled, externalized-React plugin model.
## The trick: externalize React
The output bundle must **not** carry its own copy of React. Two React instances
on one page break hooks ("invalid hook call"). Instead, mark `react`, `react-dom`
and `react/jsx-runtime` **external** and map them to Orbit's globals, so your
plugin shares the host's single React instance:
| Module | Maps to |
|---|---|
| `react` | `Orbit.React` |
| `react-dom` | `Orbit.ReactDOM` |
| `react/jsx-runtime` | `Orbit.jsxRuntime` |
Because your component is created with Orbit's React and rendered through a slot
into Orbit's tree, **hooks, state, context and effects all work**.
## Vite config
```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
lib: { entry: 'src/index.tsx', formats: ['iife'], name: 'MyOrbitPlugin',
fileName: () => 'my-plugin.js' },
rollupOptions: {
external: ['react', 'react-dom', 'react/jsx-runtime'],
output: { globals: {
react: 'Orbit.React',
'react-dom': 'Orbit.ReactDOM',
'react/jsx-runtime': 'Orbit.jsxRuntime',
} },
},
},
});
```
## Write normal TSX
```tsx
import { useState } from 'react';
Orbit.plugin('my-plugin', (orbit) => {
orbit.addUi('composer_button', () => <ShrugButton orbit={orbit} />);
orbit.addSettingsSection({
label: 'My plugin', icon: '🧩',
render: () => <Panel orbit={orbit} />,
});
});
function Panel({ orbit }) {
const [count, setCount] = useState(() => orbit.storage.get('count', 0));
const bump = () => { const n = count + 1; setCount(n); orbit.storage.set('count', n); };
return (
<div className="scard"><div className="scard__body">
<div className="sfield"><div className="sfield__intro">
Compiled plugin — real React with hooks. You are <b>{orbit.state.nick()}</b>.
</div></div>
<div className="modal__actions">
<button className="upbtn upbtn--primary" onClick={bump}>Clicked {count}×</button>
</div>
</div></div>
);
}
```
The compiled bundle is tiny — it contains only your code, calling Orbit's React.
## Starter template
A ready-to-copy starter (Vite config with the externals set up, `tsconfig`,
ambient types and a working example) lives in the repo at
[`plugin-template/`](https://codeberg.org/reversefr/orbit/src/branch/main/plugin-template).
```bash
git clone https://codeberg.org/reversefr/orbit.git
cd orbit/plugin-template
npm install
npm run build # → dist/orbit-plugin-template.js
```
## Deploy
1. Copy the built `.js` to where Orbit can fetch it (e.g. `/app/plugins/`).
2. List it in the deployed `config.json` — no rebuild of Orbit needed:
```json
{ "plugins": ["/app/plugins/my-plugin.js"] }
```
3. Reload. Your composer button and Settings section appear.

44
docs/config.md Normal file
View file

@ -0,0 +1,44 @@
# config.json reference
Orbit is configured at **runtime** by `config.json`. It is fetched on startup, deep-merged over the
built-in defaults, then the app renders — so you can re-point or rebrand **without rebuilding**.
- **Source of truth:** `public/config.json` (copied into the build).
- **Served at:** `/app/config.json`.
- **Edit live:** the service worker serves it *network-first*, so editing the deployed file takes
effect on the next reload — no rebuild.
Merge rules: objects merge key-by-key; **arrays and scalars replace** wholesale.
## Options
| Key | Type | What it does |
|-----|------|--------------|
| `server.url` | string | WebSocket URL of the IRCv3 server. |
| `startup.channels` | string[] | Channels auto-joined (first = active). A `?channel=` URL param overrides. |
| `branding.name` | string | App/network name shown in the UI + CTCP VERSION. |
| `branding.icon` | string | Logo / favicon URL. |
| `branding.url` | string | Homepage (used in CTCP VERSION/SOURCE). |
| `branding.tagline` / `taglineEm` / `subtitle` | string | Connect-screen copy. |
| `branding.projectUrl` | string | "Powered by Orbit" link in Settings. |
| `turnstile.enabled` / `sitekey` | bool / string | Cloudflare Turnstile on registration. |
| `report.target` | string | Channel that user reports are sent to. |
| `defaults.theme` | string | `light` · `dark` · `yomirc` · `yomirc-dark`. |
| `defaults.compact` / `sound` / `hideJoinQuit` / `clock24` | bool | Preset new-user prefs. |
| `features.push` / `imageUpload` / `register` | bool | Turn whole features on/off. |
> `defaults.*` only seed a user's preferences the **first** time — once someone changes a setting,
> it's stored in their browser and the config no longer overrides it.
## Example
```json
{
"server": { "url": "wss://irc.example.org/ws/" },
"startup": { "channels": ["#lobby", "#help"] },
"branding": { "name": "ExampleChat", "tagline": "Chat with", "taglineEm": "everyone." },
"turnstile":{ "enabled": false, "sitekey": "" },
"defaults": { "theme": "dark", "compact": true },
"features": { "register": false }
}
```

35
docs/deploy.md Normal file
View file

@ -0,0 +1,35 @@
# Build & deploy
Orbit builds to a **static** `dist/` you can serve from any web server.
```sh
npm run build # → dist/ (index.html + hashed assets + config.json + sw.js)
```
## Serving
Serve `dist/` under a path — conventionally `/app/`. The build's `vite.config.ts` sets
`base: '/app/'`. Example nginx:
```nginx
location /app/assets/ { root /var/www/site; expires 7d; add_header Cache-Control "immutable"; }
location = /app/index.html { root /var/www/site; add_header Cache-Control "no-cache"; }
location /app/ { root /var/www/site; try_files $uri $uri/ /app/index.html; }
```
- **Hashed assets** are immutable → cache forever.
- **`index.html`** is `no-cache` so new builds load immediately.
- **`config.json`** is served *network-first* by the service worker, so edits apply without a rebuild.
## Origin allow-list
The IRC WebSocket server must allow the **Origin** Orbit is served from (e.g. InspIRCd
`<wsorigin allow="https://chat.example.org">`). Otherwise the socket is rejected on connect.
## Service worker
On each deploy, bump the cache version in `public/sw.js` (`const CACHE = 'app-vN'`). The worker
calls `skipWaiting()` + `clients.claim()` and fires `controllerchange`, so existing PWA installs
auto-reload to the new build.
For an automated pipeline, see [Push-to-deploy](/docs/push-to-deploy/).

37
docs/ircv3.md Normal file
View file

@ -0,0 +1,37 @@
# IRCv3 capabilities
Orbit negotiates the following capabilities when the server advertises them. Most features degrade
gracefully if a cap is missing, so it works on any compliant IRCv3 server.
## Core
| Capability | Purpose |
|------------|---------|
| `sasl` | Authenticate to your account during connection. |
| `message-tags` | The basis for msgids, reactions, replies, labels. |
| `server-time` | Accurate timestamps on every message. |
| `echo-message` | The server echoes your own messages back (consistent state). |
| `batch` | Groups related messages (history, netsplits). |
| `labeled-response` | Correlate a command with its replies. |
| `account-tag` | Each message carries the sender's account (avatars, badges). |
## Presence & membership
`away-notify` · `account-notify` · `extended-join` · `chghost` · `multi-prefix` ·
`userhost-in-names` · `setname` · `invite-notify`
## Drafts (modern features)
| Capability | Feature in Orbit |
|------------|------------------|
| `draft/chathistory` + `draft/event-playback` | Server-side history replayed on join. |
| `draft/message-redaction` | Delete your own messages. |
| `draft/read-marker` | "New messages" divider, synced across devices. |
| `draft/multiline` | Send multi-line messages as one. |
| `draft/metadata-2` | Profile metadata. |
| `standard-replies` | Structured `FAIL`/`WARN`/`NOTE` handling. |
| `draft/account-registration` | Create an account from inside the client. |
| `draft/pre-away` | Set away during registration. |
| `draft/webpush` | Push notifications while the tab is closed (VAPID). |
You can list the live set any time by typing `!caps` to the bot in [#orbit](/wiki/orbit-channel/).

29
docs/overview.md Normal file
View file

@ -0,0 +1,29 @@
# Overview
**Orbit** is a modern web client for **IRC** — the open chat protocol that has quietly run
communities for decades. It runs entirely in the browser: pick a nick and you're talking, with
**no install and no signup**.
It powers [tchatou.fr](https://tchatou.fr/app/), and is built to be **re-pointed at any IRCv3
network** and **rebranded** from a single `config.json` — without recompiling.
## What makes it different
- **Full IRCv3.** Orbit negotiates 24 capabilities — SASL, message-tags, reactions, typing
indicators, read-markers, multi-line messages, server-side history, account registration, and
Web Push. See [IRCv3 capabilities](/docs/ircv3/).
- **Pluggable at runtime.** A static `config.json` is fetched on startup and sets the server,
channels, branding, themes and feature flags. One build can serve any community. See
[config.json reference](/docs/config/).
- **Installable PWA.** Mobile-first layout, offline app shell, and notifications when the tab is
closed.
- **Self-hostable.** A static build behind any web server. The project even runs its own git
server with [push-to-deploy](/docs/push-to-deploy/).
- **Free software.** AGPL-3.0 — use it, modify it, self-host it.
## Tech
React 19 · TypeScript · Vite · zustand. Tested with Vitest, linted, and built in CI. The codebase
is a clean modular tree — see [Architecture](/docs/architecture/).
> New here? Jump to the [Quick start](/docs/quick-start/) to run your own in a minute.

99
docs/plugins.md Normal file
View file

@ -0,0 +1,99 @@
# Plugin system
Orbit has a small, **operator-controlled** plugin system. A deployment lists
plugin scripts in `config.json`; each is loaded at startup and registers against
a global `Orbit` object to hook events, read state, send IRC, theme the UI, and
add UI in predefined slots.
> **Experimental.** Orbit is a work in progress and this API may change between
> releases. Plugins are deployment-controlled — same trust level as the app
> itself. There is no user-uploaded plugin marketplace, by design.
## Enabling plugins
Add script URLs to `plugins` in [`config.json`](/docs/config/):
```json
{ "plugins": ["/app/plugins/orbit-demo.js"] }
```
They load in order, after the app boots. Host them anywhere the page can reach
(same-origin recommended).
## A quick plugin
A plugin is a plain `.js` file that calls `Orbit.plugin()`. UI is authored with
the `html` tagged template (runtime markup, no build step), or with
`orbit.h(...)` (`React.createElement`).
```js
Orbit.plugin('my-plugin', (orbit, log) => {
log('loaded, Orbit v' + orbit.version);
orbit.on('message', (m) => log(m.from, 'said', m.text, 'in', m.target));
orbit.addUi('composer_button', () =>
orbit.html`<button class="composer__emoji" title="Shrug"
onClick=${() => orbit.irc.say('¯\\_(ツ)_/¯')}>🤷</button>`);
});
```
For anything substantial — real components with state — build a compiled plugin
instead. See **[Compiled plugins](/docs/compiled-plugins/)**.
## The `Orbit` API
### Global
| Member | Description |
|---|---|
| `Orbit.version` / `Orbit.commit` | app version + git commit (build-time) |
| `Orbit.plugin(name, fn)` | register a plugin; `fn(orbit, log)` |
| `Orbit.on/once/off/emit(event, …)` | the app event bus |
| `Orbit.config()` | the resolved runtime config |
| `Orbit.React` / `Orbit.ReactDOM` / `Orbit.jsxRuntime` / `Orbit.Fragment` / `Orbit.h` / `Orbit.html` | render primitives (externalization targets for compiled plugins) |
### Inside `Orbit.plugin(name, (orbit) => …)`
| Member | Description |
|---|---|
| `orbit.on/once/off/emit` | event bus (see events below) |
| `orbit.state.active()` | active buffer name |
| `orbit.state.nick()` / `account()` | your nick / logged-in account |
| `orbit.state.buffers()` | open buffer names |
| `orbit.state.get()` | full store snapshot (read-only) |
| `orbit.irc.send(line)` | send a raw IRC line |
| `orbit.irc.msg(target, text)` | PRIVMSG a target |
| `orbit.irc.say(text)` | send to the active buffer |
| `orbit.irc.join(chan)` / `part(chan)` | join / part |
| `orbit.irc.list()` | request the channel list |
| `orbit.themes.current()/list()/set(id)` | read/set the theme |
| `orbit.storage.get(key, def)/set(key, val)` | namespaced persistence |
| `orbit.addUi(slot, render)` | add UI to a slot (returns a remover) |
| `orbit.addSettingsSection({label, icon?, render})` | add a whole Settings section |
| `orbit.h / orbit.html` | render helpers |
| `log(…)` | namespaced console logger |
### Events
`ready`, `connected` (`{nick}`), `status` (connection status string),
`buffer.active` (buffer name), `message` (`{from, target, text, self}`),
`raw` (the parsed IRC message).
### UI slots
| Slot | Where |
|---|---|
| `composer_button` | a button in the message composer toolbar |
| `settings_section` | a whole section in Settings (own nav entry + pane) — use `orbit.addSettingsSection()` |
More slots (message decorators, side panels) will be added as the core grows
stable homes for them.
## Trust & security
Plugins are **operator-controlled**: a deployment lists them in `config.json`, so
they run with the same trust as the app itself. There is no user-uploaded plugin
mechanism. Orbit deliberately does **not** expose internal modules or runtime
component replacement — that would couple plugins to internals that are still
moving. The API above is the stable surface.

38
docs/push-to-deploy.md Normal file
View file

@ -0,0 +1,38 @@
# Push-to-deploy
Orbit ships its own self-hosted git workflow: a `git push` runs the tests, builds, and publishes —
no external CI required.
## How it works
A **bare git repo** on the server has a `post-receive` hook. On a push to `main` it:
1. Checks out the new commit.
2. Runs `npm ci && npm run test && npm run build`**gated**, so a broken push never ships.
3. Publishes `dist/` to the web root with `rsync --delete` (keeping a backup for rollback).
The whole loop is one command:
```sh
git commit -m "…"
git push # test → build → deploy (+ mirror)
```
## The hook (sketch)
```bash
#!/bin/bash
set -e; unset GIT_DIR
WORK=/path/to/checkout; WEBROOT=/var/www/site/app
while read -r _old new ref; do
[ "$ref" = "refs/heads/main" ] || continue
cd "$WORK"; git fetch -q self; git reset --hard -q self/main
npm ci --silent && npm run test && npm run build
rsync -a --delete dist/ "$WEBROOT/"
done
```
## Bonus: IRC announcements
The same hook hands the commit list to a small bot that posts a GitHub-style summary to the
project's IRC channel. See [The #orbit channel & bot](/wiki/orbit-channel/).

46
docs/quick-start.md Normal file
View file

@ -0,0 +1,46 @@
# Quick start
Run your own Orbit in about a minute.
## 1. Clone & install
```sh
git clone https://codeberg.org/reversefr/orbit.git
cd orbit
npm install
```
## 2. Point it at your server
Edit **`public/config.json`** — at minimum set the WebSocket URL of your IRCv3 server:
```json
{
"server": { "url": "wss://irc.example.org/ws/" },
"startup": { "channels": ["#lobby"] }
}
```
Your IRC server needs a **WebSocket listener** speaking the `text.ircv3.net` / `binary.ircv3.net`
sub-protocols (InspIRCd, Ergo, etc.), and its Origin allow-list must include where you serve Orbit.
## 3. Develop
```sh
npm run dev # http://localhost:5173
```
## 4. Build & serve
```sh
npm run build # → dist/
```
Serve the static `dist/` behind any web server (commonly under `/app/`). See
[Build & deploy](/docs/deploy/) for nginx and service-worker details.
## Next steps
- [config.json reference](/docs/config/) — every option.
- [Branding & themes](/docs/branding/) — make it yours.
- [IRCv3 capabilities](/docs/ircv3/) — what works on your server.