feat: relay-grouped participant rendering + relay_label in protocol
Some checks failed
Mirror to GitHub / mirror (push) Failing after 36s
Build Release Binaries / build-amd64 (push) Failing after 1m47s

RoomParticipant now has optional relay_label field. Desktop client
groups participants by relay: "This Relay" (green dot) for local,
peer label (blue dot) for federated. Shows all relays in the chain
including intermediate ones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-08 11:22:05 +04:00
parent 7bddc6b5a6
commit da593f9510
5 changed files with 74 additions and 19 deletions

View File

@@ -670,6 +670,10 @@ pub struct RoomParticipant {
pub fingerprint: String,
/// Optional display name set by the client.
pub alias: Option<String>,
/// 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<String>,
}
/// Reasons for ending a call.

View File

@@ -45,6 +45,7 @@ unsafe impl Sync for SyncWrapper {}
pub struct ParticipantInfo {
pub fingerprint: String,
pub alias: Option<String>,
pub relay_label: Option<String>,
}
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

View File

@@ -18,6 +18,7 @@ struct CallEvent {
struct Participant {
fingerprint: String,
alias: Option<String>,
relay_label: Option<String>,
}
#[derive(Clone, Serialize)]
@@ -195,6 +196,7 @@ async fn get_status(state: tauri::State<'_, Arc<AppState>>) -> Result<CallStatus
.map(|p| Participant {
fingerprint: p.fingerprint,
alias: p.alias,
relay_label: p.relay_label,
})
.collect(),
encode_fps: status.frames_sent,

View File

@@ -540,32 +540,49 @@ async function pollStatus() {
const pct = rms > 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 = '<div class="participants-empty">Waiting for participants...</div>';
} 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<string, typeof st.participants> = {};
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 = `<span class="relay-dot-small ${isLocal ? "green" : "blue"}"></span> ${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 = `
<div class="name">${escapeHtml(name)} ${isMe ? '<span class="you-badge">you</span>' : ""}</div>
<div class="fp">${escapeHtml(fp ? fp.substring(0, 16) : "")}</div>
`;
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 = `
<div class="name">${escapeHtml(name)} ${isMe ? '<span class="you-badge">you</span>' : ""}</div>
<div class="fp">${escapeHtml(fp ? fp.substring(0, 16) : "")}</div>
`;
row.appendChild(info);
participantsDiv.appendChild(row);
});
});
}

View File

@@ -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;