feat: relay-grouped participant rendering + relay_label in protocol
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:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user