Outgoing voice was gated on the observer's roster copy of the remote user's
voice state, which is signaling gossip. The signal server broadcasts
`user_left` for any socket it declares dead, so a suspended laptop or a flaky
hop wiped that copy and the observer detached its microphone from a peer that
never left the channel - a silent member with no way back through the UI.
`decideVoicePathRouting` now closes a path only on positive evidence: we left
voice, the peer itself reported another channel or none, or the connection is
gone. Missing gossip holds an established path instead. Opening still needs
confirmation, so a guess never starts sending; the same rule gates playback,
camera video, and the microphone a new connection puts in its first offer.
Peers are also asked for their voice state when a connection or data channel
comes up, so a rebuilt path re-confirms itself.
Alongside it, the microphone can be switched mid-call: capture moves to a
device service and rules, the live track is swapped with `replaceTrack` so
the session is never renegotiated, and the speaking indicator follows the new
stream.
Peer recovery burned its whole retry budget while signaling was down, then
dropped the tracker with no re-arm, so a peer stayed dead until an unrelated
roster event healed it. Recovery now waits for a usable transport before
spending an attempt.
Initiator election also compared a home actor id against foreign roster ids,
which is not antisymmetric across signal servers - both sides offered, or
neither did. Election moves into `peer-role.rules` and compares ids only
within one signal server's identity space.
A client that could not authorize against a foreign signal server was
redirected into a dead end with no way to retry, so servers joined from
another signal route became unreachable.
The home server now stores a per-user provision secret, clients keep it in
their own store, and a recovery service records why authorization failed per
server URL. Invite, server browser, and chat room surface that reason and
offer a retry instead of silently redirecting.
- `LIVE_RELOAD=false npm run dev` keeps the renderer alive across a machine
suspend; the reload client otherwise destroys the session under test.
- `dev-peer.sh` plus a separate userdata dir runs a second local peer.
- `tools/voice-probe.js` samples peer state and RTP counters from a live
window, persisting to localStorage so a renderer reload cannot erase it.
- e2e helpers for voice pairs, peer-role election, and a TURN relay.
- Electron single-instance and dev-client-load decisions move into rules
files with colocated specs.
Match incoming direct-call events against every local identity alias - home
id, entity id, peer id, and each provisioned signal-server actor id - instead
of only oderId||id. A caller who met the callee through a room on the
caller's signal server addresses the ring by the callee's provisioned actor
id, so the old admission check silently dropped it: the caller went "In
Voice" while the callee saw no modal, no ring audio, and no rail entry.
Incoming self aliases are normalized onto the canonical local id
(normalizeDirectCallPayloadSelfAliases) so they never appear as a phantom
third participant, and remoteParticipantIds / the DM-header peer lookup skip
all self aliases.
Adds a DM-header call ring e2e including the cross-signal topology (callee
homed on a secondary signal server) that fails on the old code.
Co-authored-by: Cursor <cursoragent@cursor.com>
Remove the leftover MAX_AUTO_SAVE_SIZE_BYTES guard from handleFileChunk's
in-memory path. The request gate (canReceiveAttachment) already admits 10-50 MB
generic files for in-memory receive on stores without disk streaming (browser),
but the chunk handler silently dropped every chunk of such files: no ack was
sent, the sender's waitForAck timed out, and the receiver's GUI never changed.
Receive admission is now decided once, at request time.
Adds a two-browser regression e2e that sends an 11 MB generic file and asserts
Request -> progress -> Download.
Co-authored-by: Cursor <cursoragent@cursor.com>
Hydrate playable media after disk receive, relay file-announce to sibling
devices via account_sync, bind DM attachments to pre-allocated message ids,
and improve gallery retry/cancel UX with bounded parallel auto-downloads.
Co-authored-by: Cursor <cursoragent@cursor.com>
Broadcast a cleared voice_state when voice-active sockets drop and reset mute/deafen flags on disconnect or reconnect so stale session state cannot leak to other clients.
Co-authored-by: Cursor <cursoragent@cursor.com>
Collapse home and signal-server actor aliases into one canonical room member so multi-device sessions no longer duplicate the local user in the members panel.
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose settings logout on mobile where the title bar is hidden, and enable
Capacitor data settings with storage visibility and local erase/sign-out.
Co-authored-by: Cursor <cursoragent@cursor.com>
Skip authorize login navigation when a signal server endpoint is offline or
unreachable; gate connection and credential flows on online status only.
Co-authored-by: Cursor <cursoragent@cursor.com>
Scale launcher and splash brand assets to the adaptive-icon safe zone and a smaller splash ratio so circular launcher masks and the launch splash no longer crop the cat face.
Co-authored-by: Cursor <cursoragent@cursor.com>
Remove member-ordering and complexity eslint-disable comments by reordering
class members and applying targeted fixes. Add metoyou/no-maybe-in-naming,
type-safe WebRTC e2e harness helpers, and resolve remaining lint errors so
npm run lint exits cleanly.
Co-authored-by: Cursor <cursoragent@cursor.com>
Bind custom emoji library membership to the signed-in user instead of the
client. CustomEmojiService now tracks saved emoji ids per user id in
localStorage (metoyou_custom_emoji_saved:<userId>) and the picker only shows
the active user's set, seeded on first load from legacy savedByUser rows the
user created. This stops a second account on the same client (or Electron's
shared SQLite database) from inheriting another user's emoji picker, while
keeping synced assets available for message rendering.
Adds unit coverage for per-user scoping and a single-page-load Playwright e2e
that switches users client-side (second user joins the first user's server)
and asserts no library leak.
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-out mobile visitors landing on / or /dashboard were intentionally
kept on a logged-out /dashboard, so they were never greeted with a login
screen on startup. Replace the imperative startup-redirect logic in
App.ngOnInit with a platform-agnostic pure rule
resolveUnauthenticatedStartupRedirect: non-public routes redirect to
/login (with a safe returnUrl), public routes (/login, /register,
/invite/...) are left alone. Mobile is no longer special-cased.
- Unit: auth-navigation.rules.spec.ts
- E2E: e2e/tests/mobile/mobile-login-on-startup.spec.ts (mobile viewport
set before navigation; /dashboard and / both land on /login)
Co-authored-by: Cursor <cursoragent@cursor.com>
Gate the "Shared from your device" label and the hidden download
affordance on whether this device actually holds the file bytes, not on
whether the current user uploaded it. uploaderPeerId is the user id, so
the old check claimed ownership on every device of the uploader,
blocking view/download on second devices that only synced metadata.
Also include attachment metadata in the account_sync chat-sync-batch so
sibling devices learn about synced attachments at all.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add AGENTS.md, CLAUDE.md, and the agents-docs/ tree (workflow, lessons,
engineering standards, context map, ADR seed, feature template) plus a
domain-bearing CONTEXT.md for each of the six subdomains: toju-app,
electron, server, e2e, website, docs-site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>