diff --git a/crates/wzp-proto/src/packet.rs b/crates/wzp-proto/src/packet.rs index c35c2ff..47447f4 100644 --- a/crates/wzp-proto/src/packet.rs +++ b/crates/wzp-proto/src/packet.rs @@ -670,6 +670,10 @@ pub struct RoomParticipant { pub fingerprint: String, /// Optional display name set by the client. pub alias: Option, + /// Relay label — identifies which relay this participant is connected to. + /// None for local participants, Some("Relay B") for federated. + #[serde(default)] + pub relay_label: Option, } /// Reasons for ending a call. diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index e06aabc..692159d 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -45,6 +45,7 @@ unsafe impl Sync for SyncWrapper {} pub struct ParticipantInfo { pub fingerprint: String, pub alias: Option, + pub relay_label: Option, } pub struct EngineStatus { @@ -346,6 +347,7 @@ impl CallEngine { .map(|p| ParticipantInfo { fingerprint: p.fingerprint, alias: p.alias, + relay_label: p.relay_label, }) .collect(); let count = unique.len(); @@ -395,6 +397,7 @@ impl CallEngine { .map(|p| ParticipantInfo { fingerprint: p.fingerprint.clone(), alias: p.alias.clone(), + relay_label: p.relay_label.clone(), }) .collect() }; // lock dropped here diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 9958a09..1ce3568 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -18,6 +18,7 @@ struct CallEvent { struct Participant { fingerprint: String, alias: Option, + relay_label: Option, } #[derive(Clone, Serialize)] @@ -195,6 +196,7 @@ async fn get_status(state: tauri::State<'_, Arc>) -> Result 0 ? Math.min(100, (Math.log(rms) / Math.log(32767)) * 100) : 0; levelBar.style.width = `${pct}%`; - // Participants with identicons + // Participants grouped by relay if (st.participants.length === 0) { participantsDiv.innerHTML = '
Waiting for participants...
'; } else { participantsDiv.innerHTML = ""; - st.participants.forEach((p) => { - const name = p.alias || "Anonymous"; - const fp = p.fingerprint || ""; - const isMe = fp && myFingerprint.includes(fp); + // Group by relay_label (null = this relay) + const groups: Record = {}; + st.participants.forEach((p: any) => { + const relay = p.relay_label || "This Relay"; + if (!groups[relay]) groups[relay] = []; + groups[relay].push(p); + }); - const row = document.createElement("div"); - row.className = "participant"; + Object.entries(groups).forEach(([relay, members]) => { + // Relay header + const header = document.createElement("div"); + header.className = "relay-group-header"; + const isLocal = relay === "This Relay"; + header.innerHTML = ` ${escapeHtml(relay)}`; + participantsDiv.appendChild(header); - // Identicon avatar - const icon = createIdenticonEl(fp || name, 36, true); - if (isMe) icon.style.outline = "2px solid var(--accent)"; - row.appendChild(icon); + // Participants under this relay + (members as any[]).forEach((p) => { + const name = p.alias || "Anonymous"; + const fp = p.fingerprint || ""; + const isMe = fp && myFingerprint.includes(fp); - const info = document.createElement("div"); - info.className = "info"; - info.innerHTML = ` -
${escapeHtml(name)} ${isMe ? 'you' : ""}
-
${escapeHtml(fp ? fp.substring(0, 16) : "")}
- `; - row.appendChild(info); - participantsDiv.appendChild(row); + const row = document.createElement("div"); + row.className = "participant"; + + const icon = createIdenticonEl(fp || name, 36, true); + if (isMe) icon.style.outline = "2px solid var(--accent)"; + row.appendChild(icon); + + const info = document.createElement("div"); + info.className = "info"; + info.innerHTML = ` +
${escapeHtml(name)} ${isMe ? 'you' : ""}
+
${escapeHtml(fp ? fp.substring(0, 16) : "")}
+ `; + row.appendChild(info); + participantsDiv.appendChild(row); + }); }); } diff --git a/desktop/src/style.css b/desktop/src/style.css index 5d03216..33984be 100644 --- a/desktop/src/style.css +++ b/desktop/src/style.css @@ -441,6 +441,35 @@ button.primary:disabled { opacity: 0.5; cursor: not-allowed; } border-radius: 8px; } +/* ── Relay group headers ── */ +.relay-group-header { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-dim); + padding: 6px 0 2px; + border-top: 1px solid #ffffff08; + margin-top: 4px; +} + +.relay-group-header:first-child { + border-top: none; + margin-top: 0; +} + +.relay-dot-small { + width: 6px; + height: 6px; + border-radius: 50%; + display: inline-block; +} + +.relay-dot-small.green { background: var(--green); } +.relay-dot-small.blue { background: #60a5fa; } + /* ── Controls ── */ .controls { display: flex;