feat(ui): phone-style layout for direct calls
The call screen now shows two different layouts depending on whether the call is a 1:1 direct call or a room/group call: **Direct call (directCallPeer set):** - Large centered identicon (96px circular with glow) - Peer name (22px bold) + fingerprint (11px mono) - Connection badge: "P2P Direct" (green), "Via Relay" (blue), or "Connecting..." (yellow) — auto-detected from the call-debug buffer's dual_path_race_won event - Room name header shows the peer's alias/fp instead of "general" - Group participant list is hidden **Room/group call (directCallPeer null):** - Existing group participant list layout — unchanged The badge updates live from pollStatus by scanning the debug buffer for the connect:dual_path_race_won event. If the path was "Direct" → green P2P badge; if "Relay" → blue relay badge. Before the race resolves, shows yellow "Connecting...". directCallView is cleared on showConnectScreen (call end). CSS in style.css: .direct-call-view, .dc-identicon, .dc-name, .dc-fp, .dc-badge with .relay and .connecting modifiers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -111,6 +111,16 @@
|
||||
<div class="level-meter">
|
||||
<div id="level-bar" class="level-bar-fill"></div>
|
||||
</div>
|
||||
<!-- Direct-call phone layout — shown instead of the group
|
||||
participant list when directCallPeer is set. Centered
|
||||
identicon, name, fp, connection badge. Hidden for
|
||||
room calls (directCallPeer == null). -->
|
||||
<div id="direct-call-view" class="direct-call-view hidden">
|
||||
<div id="dc-identicon" class="dc-identicon"></div>
|
||||
<div id="dc-name" class="dc-name">Unknown</div>
|
||||
<div id="dc-fp" class="dc-fp"></div>
|
||||
<div id="dc-badge" class="dc-badge">Connecting...</div>
|
||||
</div>
|
||||
<div id="participants" class="participants"></div>
|
||||
<div class="controls">
|
||||
<button id="mic-btn" class="control-btn" title="Toggle Mic (m)">
|
||||
|
||||
@@ -169,6 +169,11 @@ const callTimer = document.getElementById("call-timer")!;
|
||||
const callStatus = document.getElementById("call-status")!;
|
||||
const levelBar = document.getElementById("level-bar")!;
|
||||
const participantsDiv = document.getElementById("participants")!;
|
||||
const directCallView = document.getElementById("direct-call-view")!;
|
||||
const dcIdenticon = document.getElementById("dc-identicon")!;
|
||||
const dcName = document.getElementById("dc-name")!;
|
||||
const dcFp = document.getElementById("dc-fp")!;
|
||||
const dcBadge = document.getElementById("dc-badge")!;
|
||||
const micBtn = document.getElementById("mic-btn")!;
|
||||
const micIcon = document.getElementById("mic-icon")!;
|
||||
const spkBtn = document.getElementById("spk-btn")!;
|
||||
@@ -846,7 +851,25 @@ let directCallPeer: { fingerprint: string; alias: string | null } | null = null;
|
||||
function showCallScreen() {
|
||||
connectScreen.classList.add("hidden");
|
||||
callScreen.classList.remove("hidden");
|
||||
roomName.textContent = roomInput.value;
|
||||
|
||||
// Direct call → phone-style layout; room call → group layout.
|
||||
if (directCallPeer) {
|
||||
const fp = directCallPeer.fingerprint || "";
|
||||
const alias = directCallPeer.alias;
|
||||
roomName.textContent = alias || fp.substring(0, 16) || "Direct Call";
|
||||
dcName.textContent = alias || "Unknown";
|
||||
dcFp.textContent = fp;
|
||||
dcIdenticon.innerHTML = "";
|
||||
dcIdenticon.appendChild(createIdenticonEl(fp || "?", 96, true));
|
||||
dcBadge.textContent = "Connecting...";
|
||||
dcBadge.className = "dc-badge connecting";
|
||||
directCallView.classList.remove("hidden");
|
||||
participantsDiv.classList.add("hidden");
|
||||
} else {
|
||||
roomName.textContent = roomInput.value;
|
||||
directCallView.classList.add("hidden");
|
||||
participantsDiv.classList.remove("hidden");
|
||||
}
|
||||
callStatus.className = "status-dot";
|
||||
statusInterval = window.setInterval(pollStatus, 250);
|
||||
// Sync the Speaker/Earpiece label with the OS state (Android only; on
|
||||
@@ -984,23 +1007,38 @@ async function pollStatus() {
|
||||
const pct = rms > 0 ? Math.min(100, (Math.log(rms) / Math.log(32767)) * 100) : 0;
|
||||
levelBar.style.width = `${pct}%`;
|
||||
|
||||
// Participants grouped by relay. For direct P2P calls the
|
||||
// relay never sends a RoomUpdate (neither peer joins the
|
||||
// relay's media room) so st.participants is empty. If we
|
||||
// have a directCallPeer from the signal plane, inject a
|
||||
// synthetic entry so the UI shows who we're talking to.
|
||||
const displayParticipants =
|
||||
st.participants.length === 0 && directCallPeer
|
||||
? [{ ...directCallPeer, relay_label: "P2P Direct" }]
|
||||
: st.participants;
|
||||
// Direct-call phone-style layout: update the connection
|
||||
// badge from the call-debug buffer or from participants.
|
||||
if (directCallPeer) {
|
||||
// Check the debug buffer for the race result to label
|
||||
// the connection type (P2P Direct vs Relay).
|
||||
const raceWon = callDebugBuffer.find((e) => e.step === "connect:dual_path_race_won");
|
||||
const engineOk = callDebugBuffer.find((e) => e.step === "connect:call_engine_started");
|
||||
if (engineOk) {
|
||||
if (raceWon?.details?.path === "Direct") {
|
||||
dcBadge.textContent = "P2P Direct";
|
||||
dcBadge.className = "dc-badge";
|
||||
} else {
|
||||
dcBadge.textContent = "Via Relay";
|
||||
dcBadge.className = "dc-badge relay";
|
||||
}
|
||||
}
|
||||
// Skip the group participant rendering — direct-call
|
||||
// view is already visible and showing the peer.
|
||||
}
|
||||
|
||||
if (displayParticipants.length === 0) {
|
||||
// Participants grouped by relay (group/room calls only).
|
||||
// Hidden when directCallPeer is set — the phone-style
|
||||
// layout above handles the 1:1 display.
|
||||
if (directCallPeer) {
|
||||
// no-op: direct call view handles it
|
||||
} else if (st.participants.length === 0) {
|
||||
participantsDiv.innerHTML = '<div class="participants-empty">Waiting for participants...</div>';
|
||||
} else {
|
||||
participantsDiv.innerHTML = "";
|
||||
// Group by relay_label (null = this relay)
|
||||
const groups: Record<string, typeof st.participants> = {};
|
||||
displayParticipants.forEach((p: any) => {
|
||||
st.participants.forEach((p: any) => {
|
||||
const relay = p.relay_label || "This Relay";
|
||||
if (!groups[relay]) groups[relay] = [];
|
||||
groups[relay].push(p);
|
||||
|
||||
@@ -371,7 +371,65 @@ button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
transition: width 0.1s ease-out;
|
||||
}
|
||||
|
||||
/* ── Participants ── */
|
||||
/* ── Direct call phone-style layout ── */
|
||||
.direct-call-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
padding: 32px 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
.dc-identicon {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 0 24px rgba(74, 222, 128, 0.15);
|
||||
}
|
||||
.dc-identicon canvas,
|
||||
.dc-identicon svg,
|
||||
.dc-identicon img {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
display: block;
|
||||
}
|
||||
.dc-name {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
}
|
||||
.dc-fp {
|
||||
font-size: 11px;
|
||||
font-family: ui-monospace, Menlo, Monaco, 'Courier New', monospace;
|
||||
color: var(--text-dim);
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
max-width: 280px;
|
||||
}
|
||||
.dc-badge {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: rgba(74, 222, 128, 0.12);
|
||||
color: var(--green);
|
||||
}
|
||||
.dc-badge.relay {
|
||||
background: rgba(96, 165, 250, 0.12);
|
||||
color: #60a5fa;
|
||||
}
|
||||
.dc-badge.connecting {
|
||||
background: rgba(250, 204, 21, 0.12);
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
/* ── Participants (group call layout) ── */
|
||||
.participants {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius);
|
||||
|
||||
Reference in New Issue
Block a user