Files
tajniak81andClaude Opus 5.5 829b236efe
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
CI / Desktop client (ubuntu-24.04) (push) Canceled after 0s
CI / Desktop client (windows-latest) (push) Canceled after 0s
Desktop: notifications, tray, auto-update, installer; web: sliding sync, voice messages, notifications
Web client (also what the desktop app runs):

- Simplified Sliding Sync (MSC4186) when the homeserver advertises
  org.matrix.simplified_msc3575. src/matrix/sliding.ts answers the SDK's
  GET /sync with a sliding sync request and rewrites the response into
  the /sync shape, as the Android app does; the SDK's own sliding sync
  mode doesn't sort thread replies into threads. 20 rooms first, then
  every room (up to 1000); to_device, e2ee, account data, receipts and
  typing extensions; M_UNKNOWN_POS starts a new connection. The SDK's
  /members?at=<sync token> drops the token, which here is ours. Settings
  can switch back to /sync.
- Notifications for new messages and incoming calls while Warp isn't in
  front, by the account's push rules, evaluated after decryption. The
  browser's Notification API once turned on in Settings, or the desktop
  app's bridge (window.warpDesktop). One per room; a click opens it.
- Voice messages: record from the composer, sent as m.audio with
  MSC3245's voice marker and MSC1767's duration and waveform (encrypted
  in encrypted rooms); a waveform player in the timeline.
- @warp/ui: a slot for extra composer buttons; bell-off, play and pause
  icons.

Desktop app:

- System notifications through Wails' notifications service, fed by
  /_desktop/bridge.js (/_desktop/notify, JSON only); clicking one shows
  the window and opens the room.
- Tray icon with Open and Quit; closing the window hides it (a one-time
  notification says so), File > Quit ends it. The page title goes to
  the window and the tray tooltip, the unread count to the taskbar or
  Dock badge.
- Auto-update from a Wails update manifest at DESKTOP_UPDATE_URL: a quiet
  check a minute after start and every six hours, a notification when a
  newer version exists, Help > Check for updates opens the update window.
  Optional signature key (DESKTOP_UPDATE_KEY). Linux package installs
  leave updates to the package manager. build/manifest writes the
  manifest.
- Per-user NSIS installer (make desktop-windows-installer), the macOS
  bundle zipped for the updater, a desktop CI job, and a Desktop release
  workflow that builds all three platforms and the manifest.

Tested on Windows against a local homeserver: sliding sync with E2EE,
threads and a server restart; notifications (toast history), close to
tray, relaunch restoring the window; voice messages recorded, sent and
played, encrypted too; the update check finding a newer version. Not
tested: installing an update, the NSIS installer, CI, macOS and Linux.
Code signing needs certificates and isn't done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:01:02 +02:00

246 lines
12 KiB
Markdown

# Warp web client
A Matrix client for the browser: the room list, the timeline, sending
messages and files, invites, creating and joining rooms. It speaks the
plain Client-Server API through [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk),
so it works against any Matrix homeserver, and it is built on Warp's design
system, [`@warp/ui`](../ui/README.md).
The client is **optional and separate from the homeserver**, like the
[admin panel](../admin/README.md). It ships as its own small binary,
`warp-web`, with the built Vue app embedded. A Warp server runs the same
whether or not you install it.
```
browser ──► warp-web :8080 ──► warp :8008
serves the client /_matrix/client/*
/config.json /_matrix/media/*
```
By default warp-web proxies the two path prefixes a client uses and
answers `404` for everything else (federation, server keys, `/_warp/admin`),
so it is not a second way into the rest of the homeserver. The browser
only talks to warp-web: no CORS, and the homeserver URL is runtime
configuration rather than baked into the build.
If the homeserver is already public behind its own proxy, set
`-public-homeserver` instead. The browser then calls it directly,
warp-web proxies nothing, and the Content-Security-Policy allows exactly
that one origin.
## What it does
- Sign in with a password, and sign up when the homeserver has
registration on (with a registration token when it asks for one).
"Stay signed in" keeps the session in `localStorage`; without it the
session goes with the tab. Signing out logs the session out on the
homeserver.
- Room list with unread counts and mentions, invites, a filter, direct
messages with presence.
- Timeline: grouped messages, day separators, room events as one-line
notices, older history on scroll, images, video, audio and files.
Media is fetched with the access token (authenticated media, Matrix 1.11)
and shown from `blob:` URLs.
- Sending: text (`/me` for emotes), files by the attach button, paste or
drag and drop. Your message shows at once as *Sending*, then *Sent*,
then *Read* when someone else has read it; a failed one says *Not sent*
with Retry and Delete.
- Read receipts, typing notifications, deleting messages.
- Replies: *Reply* on a message puts it above the composer, and the
reply quotes it (sender and first line); the quote goes to the message.
Replies are sent without the old quoted fallback in their body
(Matrix 1.13), and the fallback in older ones is not shown.
- Reactions: six quick ones, or any other emoji typed in, from a
message's *React* button; chips under the message with counts, yours
highlighted. A chip adds your reaction or takes it back, and says who
reacted.
- Threads: *Reply in thread* opens the thread beside the room, its root
and replies, with a composer that writes into it. Replies stay out of
the room's timeline; the root shows "N replies", the last reply's
sender and time. *Threads* in the room header lists the room's threads
(`/threads`). Read receipts are per thread (MSC3771): the room is
marked read with `thread_id` "main", a thread with its root's ID, so
reading one leaves the others unread. Unread counts come per thread
from the homeserver (MSC3773) and threads with replies you haven't
read have a dot, on the root, in the list and on the *Threads* button.
- Spaces: when you're in any, the top of the room list picks *All chats*
or one space, which narrows the list to its rooms and its subspaces'.
A space's page (`/hierarchy`) lists its rooms and subspaces with *Open*
or *Join*; its admins add rooms you're in, create new ones in it, and
take rooms out. *New space* in the + menu and in the picker. Invites to
spaces show with the others.
- End-to-end encryption, with the Rust crypto stack matrix-js-sdk loads
(`matrix-sdk-crypto-wasm`): reading and sending in encrypted rooms,
files included (encrypted in the browser before they are uploaded, with
an encrypted thumbnail for large images). A message this session can't
decrypt says why, such as "sent before this session signed in", not
just "encrypted".
- Trust on every message: one from a session you haven't verified says
*Not verified*; one with something wrong (an unsigned session,
mismatched keys, plain text in an encrypted room) says what.
- Verification: verify a person once (in a room you share, your direct
message first) and every session they verify is trusted; or one session
at a time; or this session from one of your others. Either side scans
the other's QR code (the camera, in the browser) or both compare emoji.
- Warnings when someone's identity changes: a reset since you first saw
them asks you to acknowledge it; a reset since you *verified* them says
their sessions are no longer trusted, with Verify again or Withdraw
verification.
- Voice and video calls in any room, all through a LiveKit SFU (MatrixRTC,
the Element Call model; the old Matrix 1:1 VoIP is not used). Voice and
video call buttons in the room header. In a room with one other person
the call rings them: an incoming-call card with ring tone, answer, answer
without video and decline, and "Calling…" with a ringback tone for the
caller until they answer, decline or a minute passes. A call of two shows
the other person large and you in the corner; a bigger one is a grid
with a tile per person, their camera or screen, and a ring around
whoever is speaking. Mute, camera, screen sharing, leave, and a panel
that stays up while you move between rooms. A "Join call" banner while
one is going on in a room you're not in it. In an encrypted room the media
is end-to-end encrypted with per-participant keys. The room list says
when a room has a call going on, and the timeline says when one started
and ended. It needs `client_api.rtc` on the server and a LiveKit JWT
service; see [docs/calls.md](../../docs/calls.md). warp-web proxies
`/.well-known/matrix/client` for this, and `-call-origins`
(`WARP_WEB_CALL_ORIGINS`) lets the browser reach the JWT service and
LiveKit through the Content-Security-Policy.
- Encryption setup in Settings: cross-signing keys, secret storage and a
key backup in one step, behind a recovery key shown once. A new session
verifies itself with the recovery key (or another session) and restores
older messages from the backup. Reset encryption when the key is lost.
- New rooms (public ones too) and direct messages are end-to-end
encrypted by default; Create room has the switch for turning it off.
- Voice messages: the microphone button in the composer (while it's
empty) records; the bar shows the time and the level, with discard and
send. They go out as `m.audio` with MSC3245's voice marker and MSC1767's
duration and waveform, Opus in whatever container the engine records
(WebM in Chromium, Ogg in Firefox, MP4 in Safari), encrypted like any file
in an encrypted room. Received ones play in the timeline with their
waveform; click it to seek.
- Notifications for new messages and incoming calls while Warp isn't in
front: the browser's, once you turn them on in Settings (the browser asks
for permission), and the system's own in the [desktop app](../desktop/README.md).
Which messages notify is the account's push rules, the same ones that
decide what a phone is told, evaluated once a message is decrypted, so an
encrypted one shows its text. One notification per room; clicking it
opens the room.
- Simplified Sliding Sync (MSC4186) when the homeserver has it
(`org.matrix.simplified_msc3575` in `/versions`): the first sync asks for
the 20 most recent rooms with a few state events each, then widens to
every room, so the room list appears at once on a big account. It runs
underneath matrix-js-sdk's ordinary `/sync` loop (`src/matrix/sliding.ts`
rewrites each response into the `/sync` shape, as the Android app does),
because the SDK's own sliding sync mode doesn't sort thread replies into
their threads. Settings → This session says which sync is in use and
can switch back to `/sync`.
- Settings: display name, avatar, theme, notifications, encryption.
- Phone layout: one pane at a time, the room list is the home screen.
**Not yet:** editing messages. Notifications only while a tab is open:
there is no service worker or Web Push.
### Encryption keys in the browser
Each session keeps its keys in IndexedDB under its own name
(`warp-web-crypto:<user>:<session>`). Signing out deletes them; a session
that ends some other way (signed out elsewhere, the token revoked) has
them deleted when the page next starts. A session kept only for the tab
("Stay signed in" off) encrypts its store with a random key that lives
with its credentials in `sessionStorage`: when the tab closes the key goes
with it, and the store left behind can't be read; it is deleted after two
weeks unused. A "Stay signed in" session's store is not encrypted at rest,
like any web client's without a passphrase.
The `.wasm` file is most of the download (about 2 MB
compressed) and needs `script-src 'wasm-unsafe-eval'`, which warp-web's
Content-Security-Policy allows; it allows no JavaScript `eval`.
## Run it
### With Docker Compose
```bash
cd deploy/compose
docker compose --profile web up -d
```
The client is on <http://localhost:8080>. Leave out `--profile web` and it
isn't installed.
### As a container
```bash
docker build -t warp-web -f apps/web/Dockerfile .
docker run -p 8080:8080 -e WARP_WEB_HOMESERVER=http://warp:8008 warp-web
```
### As a binary
```bash
make web
./bin/warp-web -homeserver http://localhost:8008
```
| Flag | Environment | Default |
|---|---|---|
| `-listen` | `WARP_WEB_LISTEN` | `:8080` |
| `-homeserver` | `WARP_WEB_HOMESERVER` | `http://localhost:8008` |
| `-public-homeserver` | `WARP_WEB_PUBLIC_HOMESERVER` | empty: proxy through warp-web |
| `-log-format` | `WARP_WEB_LOG_FORMAT` | `text` (or `json`) |
`/healthz` answers `ok` for container health checks. `/config.json` is what
the client reads on start: `{"homeserver_url": "", "version": "..."}`, an
empty URL meaning "this origin".
## Deploying
- Put warp-web behind your TLS proxy. It sends a strict
Content-Security-Policy, `X-Frame-Options: DENY` and
`Referrer-Policy: no-referrer` with the client's pages.
- Keep the proxy's read timeout above 30 seconds: `/sync` long-polls.
- Serve it over HTTPS: browsers only give the page a camera (for scanning
QR codes when verifying) and WebCrypto on a secure origin.
- Allow request bodies as large as the homeserver's
`media.max_upload_bytes`; uploads pass through warp-web.
- warp-web adds `X-Forwarded-For`. The homeserver uses it for rate limits
only with `client_api.trust_x_forwarded_for` on; without it, every
client counts against warp-web's own address. Turn it on only when every
client reaches the homeserver through proxies you control.
## Develop
```bash
pnpm install
pnpm web:dev # http://localhost:5175, proxied to localhost:8008
```
`WARP_WEB_HOMESERVER` points the dev server at another homeserver. Vite
proxies the same paths warp-web does, so the app code is the same in both.
| Command | What |
|---|---|
| `make web-web` | build the frontend into `client/build/dist` |
| `make web` | build the frontend, then `bin/warp-web` with it embedded |
| `make web-test` | typecheck the frontend, vet and test the Go host |
| `make web-docker` | build the container image |
A `warp-web` compiled before the frontend is built still runs. It serves a
page saying the client is missing.
## Layout
| Path | What |
|---|---|
| `src/matrix/` | everything that touches matrix-js-sdk: the client's lifecycle, sliding sync (`sliding.ts`), the timeline projection, sending, voice recording (`voice.ts`), media, encryption (`crypto.ts`, `attachments.ts`) |
| `src/stores/` | session, room list and connection state, encryption and verification (`security.ts`), calls (`call.ts`), notifications, profile, toasts (Pinia) |
| `src/views/` | sign-in, sign-up, home, room |
| `src/components/` | timeline, message body, room info, dialogs |
| `cmd/warp-web/` | the binary |
| `host/` | static serving, `/config.json` and the allow-listed proxy (shared with [the desktop app](../desktop/README.md)) |
| `client/` | the embedded build (shared with the desktop app) |
The SDK's objects change in place and emit events. The stores turn those
events into a version counter (bumped at most once a frame), and the views
are computed from the SDK on each bump. Keeping the SDK behind `src/matrix`
and `src/stores` is what will let the desktop app put its Go backend
behind the same stores.