feat(calls): native-call lifecycle endpoints + WebView steps aside (inc 1)

Complete the WebView/server side of native LiveKit calls:
- routes.js: /api/calls/answered (markDmAnswered) + /api/calls/end (endCallByRoom) so
  the server learns a NATIVE call was answered/ended (native media runs over LiveKit,
  bypassing our mesh/WS lifecycle). Additive no-ops for WebView/mesh calls.
- home.html: for native calls the WebView no longer joins the room (one connection per
  identity — the plugin holds it). answerCall -> POST /api/calls/answered + clear invite;
  endCall -> /api/calls/end (answered) or /api/calls/decline (still ringing). Outgoing
  DM/group calls fetch a LiveKit token and hand it to NativeCall.reportOutgoingCall
  instead of enterMeeting. Removed the old callHandoff mic-repush.

Server deploys now; the plugin (native LiveKit) needs a Codemagic build. Still gated by
CALLKIT_ENABLED=0 — flip to 1 only after the build is installed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 16:32:13 +05:30
parent 3e8aaad68c
commit e60e80e4bd
2 changed files with 42 additions and 22 deletions
+20
View File
@@ -1074,6 +1074,26 @@ route('POST', '/api/calls/decline', async (req, res) => {
json(res, 200, await CALLS.declineDmCall(String(room), u));
});
// --- Native-call lifecycle. NATIVE (CallKit + LiveKit) calls carry media over LiveKit, NOT our mesh/WS, so
// the server can't learn from a room emptying that a native call was answered/ended. The app signals it
// explicitly. (WebView/mesh calls keep using the WS room lifecycle — these are additive no-ops there.) ---
route('POST', '/api/calls/answered', async (req, res) => {
const u = await currentUser(req);
if (!u) return json(res, 401, { error: 'unauthorized' });
const { room } = await readBody(req);
if (!room) return json(res, 400, { error: 'room required' });
try { CALLS.markDmAnswered(String(room), u.id); } catch (_) {}
json(res, 200, { ok: true });
});
route('POST', '/api/calls/end', async (req, res) => {
const u = await currentUser(req);
if (!u) return json(res, 401, { error: 'unauthorized' });
const { room } = await readBody(req);
if (!room) return json(res, 400, { error: 'room required' });
try { await CALLS.endCallByRoom(String(room)); } catch (_) {}
json(res, 200, { ok: true });
});
// Toggle "only admins can add/remove members" (any admin).
route('POST', '/api/groups/admin-only', async (req, res) => {
const u = await currentUser(req);