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, pub fingerprint: String,
/// Optional display name set by the client. /// Optional display name set by the client.
pub alias: Option<String>, 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. /// Reasons for ending a call.

View File

@@ -45,6 +45,7 @@ unsafe impl Sync for SyncWrapper {}
pub struct ParticipantInfo { pub struct ParticipantInfo {
pub fingerprint: String, pub fingerprint: String,
pub alias: Option<String>, pub alias: Option<String>,
pub relay_label: Option<String>,
} }
pub struct EngineStatus { pub struct EngineStatus {
@@ -346,6 +347,7 @@ impl CallEngine {
.map(|p| ParticipantInfo { .map(|p| ParticipantInfo {
fingerprint: p.fingerprint, fingerprint: p.fingerprint,
alias: p.alias, alias: p.alias,
relay_label: p.relay_label,
}) })
.collect(); .collect();
let count = unique.len(); let count = unique.len();
@@ -395,6 +397,7 @@ impl CallEngine {
.map(|p| ParticipantInfo { .map(|p| ParticipantInfo {
fingerprint: p.fingerprint.clone(), fingerprint: p.fingerprint.clone(),
alias: p.alias.clone(), alias: p.alias.clone(),
relay_label: p.relay_label.clone(),
}) })
.collect() .collect()
}; // lock dropped here }; // lock dropped here

View File

@@ -18,6 +18,7 @@ struct CallEvent {
struct Participant { struct Participant {
fingerprint: String, fingerprint: String,
alias: Option<String>, alias: Option<String>,
relay_label: Option<String>,
} }
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
@@ -195,6 +196,7 @@ async fn get_status(state: tauri::State<'_, Arc<AppState>>) -> Result<CallStatus
.map(|p| Participant { .map(|p| Participant {
fingerprint: p.fingerprint, fingerprint: p.fingerprint,
alias: p.alias, alias: p.alias,
relay_label: p.relay_label,
}) })
.collect(), .collect(),
encode_fps: status.frames_sent, 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; const pct = rms > 0 ? Math.min(100, (Math.log(rms) / Math.log(32767)) * 100) : 0;
levelBar.style.width = `${pct}%`; levelBar.style.width = `${pct}%`;
// Participants with identicons // Participants grouped by relay
if (st.participants.length === 0) { if (st.participants.length === 0) {
participantsDiv.innerHTML = '<div class="participants-empty">Waiting for participants...</div>'; participantsDiv.innerHTML = '<div class="participants-empty">Waiting for participants...</div>';
} else { } else {
participantsDiv.innerHTML = ""; participantsDiv.innerHTML = "";
st.participants.forEach((p) => { // Group by relay_label (null = this relay)
const name = p.alias || "Anonymous"; const groups: Record<string, typeof st.participants> = {};
const fp = p.fingerprint || ""; st.participants.forEach((p: any) => {
const isMe = fp && myFingerprint.includes(fp); const relay = p.relay_label || "This Relay";
if (!groups[relay]) groups[relay] = [];
groups[relay].push(p);
});
const row = document.createElement("div"); Object.entries(groups).forEach(([relay, members]) => {
row.className = "participant"; // 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 // Participants under this relay
const icon = createIdenticonEl(fp || name, 36, true); (members as any[]).forEach((p) => {
if (isMe) icon.style.outline = "2px solid var(--accent)"; const name = p.alias || "Anonymous";
row.appendChild(icon); const fp = p.fingerprint || "";
const isMe = fp && myFingerprint.includes(fp);
const info = document.createElement("div"); const row = document.createElement("div");
info.className = "info"; row.className = "participant";
info.innerHTML = `
<div class="name">${escapeHtml(name)} ${isMe ? '<span class="you-badge">you</span>' : ""}</div> const icon = createIdenticonEl(fp || name, 36, true);
<div class="fp">${escapeHtml(fp ? fp.substring(0, 16) : "")}</div> if (isMe) icon.style.outline = "2px solid var(--accent)";
`; row.appendChild(icon);
row.appendChild(info);
participantsDiv.appendChild(row); 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; 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 ── */
.controls { .controls {
display: flex; display: flex;