Found testing calls between two phones on the Compose call stack:
- The famedly SDK keeps only "important" state in memory for a room that
hasn't been opened, so call memberships of other rooms were invisible:
no ring (it was cleared at once, the call looking empty), no room-list
marker. org.matrix.msc3401.call.member is now important state.
- The incoming-call card closed itself with Navigator.pop when the ring
cleared, which popped the call screen pushed above it on Answer. The
buttons now close the card first, and it closes itself only while it
is the top route.
Tested: a voice call in a room of two (ring, answer, both connected
through LiveKit), and a video call in an encrypted room (to-device media
keys, frame cryptors OK on both sides, each phone showing the other's
video). The web client sees a phone's membership and rings for its
notification.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calls the way the web client and Element Call do them, with the same
events, so the three can share a call. The famedly SDK's own group calls
use com.famedly.call.member events, which the others don't read, so this
is written against the matrix-js-sdk wire format instead (lib/matrix/
calls.dart):
- Membership: org.matrix.msc3401.call.member state, key
_@user:server_DEVICE_m.call, with the legacy session fields the js-sdk
sends (application m.call, call_id "", membershipID, expires,
focus_active, foci_preferred). An empty delayed copy (MSC4140) is
scheduled first and pushed back every 5 s; leaving sends the empty
state and cancels it.
- Media: a LiveKit token from the JWT service (the call's, else the
homeserver's rtc/transports or .well-known rtc_foci) for an OpenID
token; livekit_client connects.
- Encrypted rooms: a random 16-byte key per device, sent to every other
device in the call over encrypted to-device
io.element.call.encryption_keys, keys from others applied per LiveKit
identity (@user:server:DEVICE); per-participant keys, no ratcheting,
HKDF, like the web client's livekit-client.
- Ringing: a call started in a room of two sends a ring notification
(MSC4075); the other side gets an incoming-call card and an insistent
ringtone notification (full screen on a locked phone), declines with
MSC4310 or answers by joining. The caller gives up after a minute or on
a decline.
- UI: voice and video buttons in the room header, a call screen (the
other person large and you in the corner, or a grid; mute, camera, flip,
leave), a Join banner and a room-list marker for calls going on.
Not tested end to end yet: it needs a LiveKit SFU and JWT service.
README: replies, reactions, threads, spaces, QR verification, sliding
sync and calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The famedly SDK only knows /sync, but its sync loop asks Client.sync for
each response. WarpClient answers that with a sliding sync request and
rewrites the response into the /sync shape (slidingToSyncJson), so
decryption, the database, timelines and notifications work unchanged.
- Two lists, rooms and direct chats (with all their members, so DM names
come out right), over a window that starts at the 20 most recent rooms
for a quick first screen and widens to every room (up to 1000).
- The state the screens and encryption need, $LAZY members and $ME, and
call memberships; timeline_limit 10, with prev_batch for history.
- Extensions: to_device (its since rides in the token), e2ee, account
data, receipts, typing.
- Rooms the user left while the connection was open become leaves;
invite_state and knock_state become invites and knocks.
- The SDK's token is ss1|<pos>|<to-device since>. A classic token, none, or
M_UNKNOWN_POS starts a new connection; a server without
org.matrix.simplified_msc3575 gets plain /sync.
Tested against Warp: first sync, encryption setup (account data and
oneShotSync through the connection), live messages, unread counts and
typing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When both sides support it, a verification offers QR codes first: this
device shows its code (in a white quiet zone, whatever the theme) and can
scan the other's with the camera (mobile_scanner; only codes that start
with MATRIX count). The side whose code was scanned confirms the other
saw it succeed. Compare emoji stays available, and is used directly when
either side has no QR support.
Adds the CAMERA permission (camera hardware not required).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mobile:
- The room list's title picks a space: All chats, or one space, which
narrows the list to its rooms (and its subspaces' rooms).
- A space browser over /hierarchy: the rooms of a space and of its
subspaces, joined or joinable, with Join; for those who may, adding an
existing room or a new one to the space.
- New space in the new-chat sheet; invites to spaces show with the other
invites and open the space when joined.
- Fixes several setState calls that were handed a Future (reloading the
members, sessions, push distributor, threads), which Flutter rejects.
Server: /hierarchy left out children_state for a room without children.
The spec requires it on every room (an empty list), and SpaceRoom's
omitempty dropped empty lists too, despite its comment. The famedly Dart
SDK fails to parse such a response. The client API now wraps each room so
the field is always written; the federation response, which wants it left
out of the children it lists, is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Replies: Reply on a message's long press puts it above the composer;
sent replies quote the message they answer (sender and first line),
with the reply fallback left out of the body.
- Reactions: six quick ones and any other on long press, chips under the
message with counts, the user's own highlighted; tapping a chip adds or
takes back a reaction, a long press lists who reacted.
- Threads: Reply in thread opens a thread view (the root, then its
replies, older ones fetched from /relations, new ones live from the room
timeline) with a composer that sends into the thread. Thread replies
are hidden from the main timeline; the root shows "N replies". Room info
lists the room's threads (/threads).
The composer moved out of the room screen into widgets/composer.dart so
the room and thread views share it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apps/mobile is a Matrix client for Android on the famedly Matrix Dart SDK,
with vodozemac (compiled from Rust during the build) for end-to-end
encryption. It speaks the plain Client-Server API and uses the web
client's design tokens, fonts and symbol.
- Sign in by server name (.well-known), homeserver address or full user ID;
sign up with the dummy and registration token stages. Sessions survive
restarts: SQLite in private storage, credentials also in the Keystore,
both excluded from backups and device transfers.
- Room list with unread and mention counts, invites, search and filters.
Timeline with grouping, day separators, notices, history on scroll,
links, images and files (authenticated media). Sending text, photos,
camera shots and files, with Sending/Sent/Read/Not sent and Retry.
Typing, receipts, deleting. New rooms and DMs, encrypted by default;
joining by address; room info with members, invite, rename, topic,
notification level, leave.
- Encryption: one-step setup behind a recovery key, verifying a new session
with the recovery key (restoring the backup) or another session, emoji
verification of people and sessions, reset. Undecryptable messages say
why. Session list with verify and sign out.
- Notifications while running, and push through UnifiedPush (no Firebase):
the distributor's endpoint becomes an event_id_only pusher, the gateway
is discovered, and a push starts the app headless to fetch, decrypt and
show the event.
Make targets: mobile (release APKs per ABI into bin/), mobile-run,
mobile-test. Kotlin incremental compilation is off (Windows, project and
pub cache on different drives), and Tink's JVM artifact is excluded in
favour of tink-android.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apps/desktop is the web client in a native window. It embeds the same
build warp-web does and serves it through the same Go package, so every
screen is shared. Unlike warp-web it proxies nothing: the client calls the
homeserver directly, and the CSP allows the homeserver plus any https:
and wss: origin for the call backend.
- First run asks for the homeserver: a server name or address, with
.well-known discovery and a /versions check; File > Change homeserver
asks again. Signed-in sessions keep their own base URL, so a change
never sends one server's token to another.
- Links to other sites open in the system browser (/_desktop/links.js,
added to index.html); only http, https and mailto are opened.
- One instance at a time, since sessions and keys live in the webview's
storage.
- Dev builds open the DevTools Protocol on Windows when
WARP_DESKTOP_REMOTE_DEBUGGING_PORT is set, for scripted testing.
- Packaging: icon, version info and manifest in the Windows exe, a macOS
.app with camera and microphone descriptions, Linux packages via nfpm.
Make targets: desktop, desktop-run, desktop-test, desktop-resources,
desktop-macos, desktop-linux.
Web client: host and client move out of internal/ so the desktop app can
import them, and host gains AnyCallOrigin. The Dockerfile, .dockerignore
and the vite output path follow the move.
Pinned to Wails v3.0.0-beta.27.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every call in the web client now goes through MatrixRTC with a LiveKit
SFU, the model Element Call and Element X use; the legacy 1:1 m.call.*
calls are gone. In a room of two the other person is rung and can answer
or decline; in a bigger room a banner offers to join the call. Calls show
a grid of participant tiles with mute, camera, screen sharing and leave,
and media is end-to-end encrypted in encrypted rooms.
Server:
- MSC4140 delayed events (send/state with a delay, plus the
delayed_events list/restart/cancel/send API), stored and scheduled, so
a member whose client drops off is taken out of the call.
- POST /user/{userId}/openid/request_token, which lk-jwt-service uses to
hand out LiveKit tokens.
- org.matrix.msc4143.rtc_foci in /.well-known/matrix/client, configured
under client_api.rtc.
- Room presets let members send call membership state.
- Migration 0014 for both SQL dialects.
Deploy: LiveKit and lk-jwt-service in compose (calls.yml) and the helm
chart. Docs: docs/calls.md, configuration and deployment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Server: GET /voip/turnServer, configured under client_api.turn. A shared
secret (coturn use-auth-secret) gives each user expiring TURN REST API
credentials; static credentials are the fallback; with nothing configured
it answers {} so clients still try a direct connection.
Web client: 1:1 calls (Matrix VoIP via matrix-js-sdk) in rooms with one
other person. Voice and video buttons in the room header, a ringing card
with answer, answer without video and decline, and a call panel that
stays up across rooms: mute, camera, screen sharing, expand, hang up,
ring tones. Call events show in the timeline and room list. Works around
the SDK keeping a failed microphone request forever.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New rooms (public too) and direct messages are end-to-end encrypted by
default.
- Per-message trust: "Not verified" for sessions you haven't verified,
and what is wrong for unsigned sessions, mismatched keys or plain text
in an encrypted room (src/matrix/trust.ts).
- Verify a person's identity in a room you share (your DM first), and
QR-code verification both ways: show ours, scan theirs with the camera
(qrcode-generator, jsqr).
- Warnings when someone's identity changes, with acknowledge, verify
again or withdraw verification.
- Encrypted thumbnails for large images.
- A tab-only session encrypts its crypto store with a key kept in
sessionStorage; stores unused for two weeks are deleted.
- Reset encryption fixed: new secret storage first, so the new
cross-signing keys survive a stale /keys/query answer; status checks
no longer query our own keys mid-setup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apps/web: a Matrix client for the browser on Vue 3, Tailwind and
matrix-js-sdk, built on @warp/ui. Like the admin panel it is an optional,
separate service: warp-web embeds the built app, serves /config.json and
proxies only /_matrix/client and /_matrix/media to the homeserver (or,
with -public-homeserver, lets the browser call it directly).
Sign-in and sign-up with registration tokens, room list, timeline with
back-pagination, text, images, video and files over authenticated media,
local echo with sent/read/failed states, receipts, typing, invites,
creating, joining and leaving rooms, room info, settings, phone layout.
End-to-end encryption through the Rust crypto: encrypted messages and
files, emoji verification, key backup setup and restore.
apps/ui: WRoomItem, WMessageBubble and WComposer from the brand package,
and the icons the client needs.
syncapi fixes found with the client:
- A room joined inside the sync window was sent as the join alone with
limited: false, so clients thought they had the whole room. It is now
read like an initial sync; a profile change is not mistaken for a join.
- A live typing notification was reported on every sync, so every long
poll returned at once while anybody typed. Typing now has a position
in the sync token and is reported when it changes, including the empty
list when the last person stops.
Also: Dockerfile, a `web` compose profile, Makefile targets, a CI job
with a proxy smoke test, docs. PLAN.md moves out of the repository.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apps/ui: Warp's design system for Vue 3 + Tailwind v4. Tokens, fonts,
icons and logo come from the brand package, with a Tailwind theme on the
tokens and Vue components on the brand component styles.
apps/admin: the admin panel as a separate, optional service. warp-admin
embeds the built Vue app and proxies only the login endpoints and
/_warp/admin/v1 to the homeserver. Pages: dashboard, users, rooms,
federation, registration and tokens, server notices, application
services, audit log.
Also: pnpm workspace, go.work entry, Dockerfile, an `admin` compose
profile, Makefile targets, a CI job with a proxy smoke test, and docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scaffolds the server module and everything Phase 0 of PLAN.md calls for:
a binary that boots, serves its discovery endpoints and nothing else yet.
- config: one YAML file plus per-field WARP_* environment overrides,
unknown fields rejected, validation that reports every problem at once
- logging and metrics: slog, a Prometheus registry, per-route HTTP
instrumentation labelled by pattern rather than path
- httputil: the Matrix error envelope, error-returning handlers, CORS,
request logging, panic recovery, and a router over http.ServeMux
- endpoints: /_matrix/client/versions, /.well-known/matrix/client,
/.well-known/matrix/server (gated), /healthz, /metrics, and
M_UNRECOGNIZED for everything else
- storage: driver registry, shared database/sql base, checksummed
migration runner, sqlite and postgres drivers, pocketbase stub
- settings: server_settings table behind a cached service, so the
federation and registration switches change without a restart
- federation: the gate that makes federation a runtime mode
- cmd/warp: serve, migrate, generate-keys, version
- Makefile, golangci-lint v2, GitHub Actions CI (Linux and Windows tests,
race on Linux, storage tests against PostgreSQL, lint, boot smoke test),
Dockerfile, example config and docs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>