Update static/js/12-calls.js

This commit is contained in:
dev 2026-10-31 08:45:58 +00:00
parent 6b5cd404df
commit 6063da595c

View File

@ -44,6 +44,20 @@ Glow.calls.ensureState = () => {
state.ignoreOffer ||= new Map();
state.isSettingRemoteAnswerPending ||= new Map();
state.panelOpen ??= false;
state.metrics ||= {
packetLoss: null,
uptime: null,
latency: null,
elapsedSeconds: 0,
userCount: 0
};
state.drag ||= {
active: false,
x: null,
y: null,
offsetX: 0,
offsetY: 0
};
};
Glow.calls.scopeLabel = invite => {
@ -182,7 +196,8 @@ Glow.calls.upsertPeer = peer => {
const normalized = {
participantId,
roomId: peer.roomId || peer.room_id || state.session?.call_id || "",
username: peer.username || participantId
username: peer.username || participantId,
connectionState: peer.connectionState || "connecting"
};
const index = peers.findIndex(item => item.participantId === participantId);
@ -301,13 +316,23 @@ Glow.calls.ensurePeerConnection = async peerId => {
};
pc.onconnectionstatechange = () => {
Glow.calls.debug("pc connection state", peerId, pc.connectionState);
const nextState = pc.connectionState || "connecting";
Glow.calls.debug("pc connection state", peerId, nextState);
const peer = state.peers.find(item => item.participantId === peerId);
if (peer) {
peer.connectionState = nextState;
}
const bad =
pc.connectionState === "failed" ||
pc.connectionState === "disconnected";
nextState === "failed" ||
nextState === "disconnected";
if (bad) {
Glow.showToast(`Connection issue with ${peerId}.`, "error");
}
Glow.calls.renderParticipants();
Glow.calls.updateSummaryMetrics();
};
pc.onsignalingstatechange = () => {
@ -475,6 +500,15 @@ Glow.calls.renderPendingInvites = () => {
wrap.classList.remove("hidden");
};
Glow.calls.getParticipantStateLabel = peer => {
const state = String(peer?.connectionState || "").toLowerCase();
if (state === "connected") return "active";
if (state === "connecting" || state === "new") return "connecting";
if (state === "disconnected") return "disconnected";
if (state === "failed") return "reconnecting";
return "joined";
};
Glow.calls.renderParticipants = () => {
if (!Glow.els.callParticipants) return;
@ -504,7 +538,7 @@ Glow.calls.renderParticipants = () => {
selfRight.className = "call-participant-status";
selfRight.textContent = state.activeCall
? state.muted
? "connected · muted"
? "connected · muted"
: "connected"
: "idle";
@ -524,12 +558,16 @@ Glow.calls.renderParticipants = () => {
const row = document.createElement("div");
row.className = "call-participant";
const left = document.createElement("div");
left.className = "call-participant-name";
const left = document.createElement("button");
left.className = "call-participant-name profile-link";
left.type = "button";
left.textContent =
participant.display_name || participant.username || `user ${participant.user_id}`;
if (participant) {
Glow.applyUserColor(left, participant);
Glow.applyUserColor(left, participant);
if (participant.username) {
left.addEventListener("click", () => {
Glow.openProfile?.(participant.username);
});
}
const right = document.createElement("div");
@ -542,7 +580,9 @@ Glow.calls.renderParticipants = () => {
);
});
right.textContent = matchingPeer ? "connected" : "joined";
const label = Glow.calls.getParticipantStateLabel(matchingPeer);
right.textContent = label;
right.dataset.state = label;
row.append(left, right);
box.append(row);
@ -560,13 +600,21 @@ Glow.calls.renderParticipants = () => {
const row = document.createElement("div");
row.className = "call-participant";
const left = document.createElement("div");
left.className = "call-participant-name";
const left = document.createElement("button");
left.className = "call-participant-name profile-link";
left.type = "button";
left.textContent = peer.username || peer.participantId;
if (peer.username) {
left.addEventListener("click", () => {
Glow.openProfile?.(peer.username);
});
}
const right = document.createElement("div");
right.className = "call-participant-status";
right.textContent = "connecting";
const label = Glow.calls.getParticipantStateLabel(peer);
right.textContent = label;
right.dataset.state = label;
row.append(left, right);
box.append(row);
@ -583,6 +631,54 @@ Glow.calls.renderParticipants = () => {
}
};
Glow.calls.updateSummaryMetrics = () => {
Glow.calls.ensureState();
const state = Glow.calls.state();
const createdAt =
state.activeCall?.groupSession?.created_at ||
state.session?.group_session?.created_at ||
null;
let elapsedSeconds = 0;
if (createdAt) {
const ms = new Date(createdAt).getTime();
if (!Number.isNaN(ms)) {
elapsedSeconds = Math.max(0, Math.floor((Date.now() - ms) / 1000));
}
}
state.metrics.elapsedSeconds = elapsedSeconds;
state.metrics.userCount =
1 + Glow.calls.listOtherPeers().length;
if (Glow.els.callUserCountStat) {
Glow.els.callUserCountStat.textContent = String(state.metrics.userCount);
}
if (Glow.els.callElapsedStat) {
const minutes = Math.floor(elapsedSeconds / 60);
const seconds = elapsedSeconds % 60;
Glow.els.callElapsedStat.textContent =
minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
}
if (Glow.els.callLatencyStat) {
Glow.els.callLatencyStat.textContent =
state.metrics.latency == null ? "—" : `${state.metrics.latency} ms`;
}
if (Glow.els.callPacketLossStat) {
Glow.els.callPacketLossStat.textContent =
state.metrics.packetLoss == null ? "—" : `${state.metrics.packetLoss}%`;
}
if (Glow.els.callUptimeStat) {
Glow.els.callUptimeStat.textContent =
state.metrics.uptime == null ? "—" : `${state.metrics.uptime}%`;
}
};
Glow.calls.connectWebSocket = async () => {
Glow.calls.ensureState();
const state = Glow.calls.state();
@ -604,6 +700,7 @@ Glow.calls.connectWebSocket = async () => {
state.reconnecting = false;
state.joining = false;
Glow.calls.setStatusText("Connected");
Glow.calls.updateSummaryMetrics();
settled = true;
resolve();
};
@ -641,6 +738,7 @@ Glow.calls.connectWebSocket = async () => {
if (signal.type === "joined") {
state.selfPeerId = signal.participantId;
Glow.calls.setStatusText("Call connected");
Glow.calls.updateSummaryMetrics();
Glow.renderCallUi();
Glow.calls.renderParticipants();
return;
@ -651,8 +749,10 @@ Glow.calls.connectWebSocket = async () => {
Glow.calls.upsertPeer({
participantId: signal.participantId,
roomId: signal.roomId,
username: signal.username || signal.participantId
username: signal.username || signal.participantId,
connectionState: "connecting"
});
Glow.calls.updateSummaryMetrics();
Glow.renderCallUi();
Glow.calls.renderParticipants();
@ -668,6 +768,7 @@ Glow.calls.connectWebSocket = async () => {
if (signal.type === "participant-left") {
Glow.calls.removePeer(signal.participantId);
Glow.calls.updateSummaryMetrics();
Glow.renderCallUi();
Glow.calls.renderParticipants();
Glow.showToast("Peer left call.");
@ -697,7 +798,8 @@ Glow.calls.connectWebSocket = async () => {
Glow.calls.upsertPeer({
participantId: from,
roomId: signal.roomId,
username: signal.username || from
username: signal.username || from,
connectionState: "connecting"
});
Glow.calls.renderParticipants();
@ -893,6 +995,13 @@ Glow.calls.resetAll = () => {
can_invite: false
};
state.panelOpen = false;
state.metrics = {
packetLoss: null,
uptime: null,
latency: null,
elapsedSeconds: 0,
userCount: 0
};
Glow.calls.debug("resetAll complete");
};
@ -927,7 +1036,7 @@ Glow.calls.scheduleReconnect = () => {
clearTimeout(state.reconnectTimer);
state.reconnecting = true;
Glow.calls.setStatusText("Reconnecting…");
Glow.calls.setStatusText("Reconnecting…");
Glow.renderCallUi();
state.reconnectTimer = setTimeout(async () => {
@ -938,7 +1047,7 @@ Glow.calls.scheduleReconnect = () => {
Glow.calls.debug("reconnect attempt", scope);
Glow.calls.resetTransportOnly();
Glow.calls.setStatusText("Reconnecting…");
Glow.calls.setStatusText("Reconnecting…");
state.session = await Glow.calls.fetchGroupCallSession(scope);
state.permissions = state.session.permissions || state.permissions;
state.scope = state.session.scope || state.scope;
@ -969,6 +1078,7 @@ Glow.calls.refreshBackendParticipants = async () => {
Glow.state.calls.permissions = data.permissions || Glow.state.calls.permissions;
Glow.state.calls.scope = data.scope || Glow.state.calls.scope;
Glow.calls.renderParticipants();
Glow.calls.updateSummaryMetrics();
Glow.renderCallUi();
Glow.calls.debug("backend participants refreshed", data);
} catch (error) {
@ -1039,15 +1149,15 @@ Glow.renderCallUi = () => {
if (!busy) {
statusText = "Idle";
} else if (state.joining) {
statusText = "Joining call…";
statusText = "Joining call…";
} else if (state.reconnecting) {
statusText = "Reconnecting…";
statusText = "Reconnecting…";
} else if (!Glow.calls.wsReady()) {
statusText = "Connecting…";
statusText = "Connecting…";
} else if (peerCount > 0) {
statusText = `Group call active · ${peerCount} peer(s)`;
statusText = `Group call active · ${peerCount} peer(s)`;
} else {
statusText = "Call open · waiting for others";
statusText = "Call open · waiting for others";
}
Glow.els.callStatus.textContent = statusText;
@ -1083,11 +1193,77 @@ Glow.renderCallUi = () => {
Glow.els.callParticipants.classList.toggle("hidden", !overlayVisible);
}
Glow.calls.updateSummaryMetrics();
Glow.calls.renderParticipants();
Glow.calls.renderPendingInvites();
Glow.calls.renderHeaderCallChip();
};
Glow.calls.applyPanelPosition = () => {
const overlay = Glow.els.callOverlay;
const drag = Glow.state.calls?.drag;
if (!overlay || !drag) return;
if (!Number.isFinite(drag.x) || !Number.isFinite(drag.y)) return;
overlay.style.left = `${drag.x}px`;
overlay.style.top = `${drag.y}px`;
overlay.style.right = "auto";
overlay.style.bottom = "auto";
overlay.style.alignItems = "flex-start";
overlay.style.justifyContent = "flex-start";
};
Glow.calls.loadPanelPosition = () => {
const x = Number(localStorage.getItem("glow_call_panel_x"));
const y = Number(localStorage.getItem("glow_call_panel_y"));
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
Glow.state.calls.drag.x = x;
Glow.state.calls.drag.y = y;
Glow.calls.applyPanelPosition();
};
Glow.calls.savePanelPosition = () => {
const drag = Glow.state.calls?.drag;
if (!drag) return;
if (!Number.isFinite(drag.x) || !Number.isFinite(drag.y)) return;
localStorage.setItem("glow_call_panel_x", String(drag.x));
localStorage.setItem("glow_call_panel_y", String(drag.y));
};
Glow.calls.beginDrag = event => {
Glow.calls.ensureState();
const overlay = Glow.els.callOverlay;
if (!overlay) return;
const rect = overlay.getBoundingClientRect();
Glow.state.calls.drag.active = true;
Glow.state.calls.drag.x = rect.left;
Glow.state.calls.drag.y = rect.top;
Glow.state.calls.drag.offsetX = event.clientX - rect.left;
Glow.state.calls.drag.offsetY = event.clientY - rect.top;
};
Glow.calls.onDrag = event => {
const drag = Glow.state.calls?.drag;
const overlay = Glow.els.callOverlay;
if (!drag?.active || !overlay) return;
const maxX = Math.max(0, window.innerWidth - overlay.offsetWidth);
const maxY = Math.max(0, window.innerHeight - overlay.offsetHeight);
drag.x = Math.min(maxX, Math.max(0, event.clientX - drag.offsetX));
drag.y = Math.min(maxY, Math.max(0, event.clientY - drag.offsetY));
Glow.calls.applyPanelPosition();
};
Glow.calls.endDrag = () => {
const drag = Glow.state.calls?.drag;
if (!drag) return;
drag.active = false;
Glow.calls.savePanelPosition();
};
Glow.startCall = async () => {
Glow.calls.ensureState();
@ -1105,7 +1281,7 @@ Glow.startCall = async () => {
try {
Glow.state.calls.joining = true;
Glow.calls.setStatusText("Preparing call…");
Glow.calls.setStatusText("Preparing call…");
const session = await Glow.calls.fetchGroupCallSession(scope);
Glow.state.calls.session = session;
@ -1129,14 +1305,15 @@ Glow.startCall = async () => {
Glow.state.calls.panelOpen = true;
Glow.openCallPanel();
Glow.calls.setStatusText("Connecting microphone…");
Glow.calls.setStatusText("Connecting microphone…");
await Glow.calls.ensureLocalAudio();
Glow.calls.setStatusText("Joining call…");
Glow.calls.setStatusText("Joining call…");
await Glow.apiJoinGroupCall(session.group_session.id);
await Glow.calls.connectWebSocket();
Glow.calls.startParticipantSync();
Glow.calls.startHeartbeat();
Glow.state.calls.joining = false;
Glow.calls.updateSummaryMetrics();
Glow.renderCallUi();
Glow.showToast("Group call ready.");
} catch (error) {
@ -1164,7 +1341,7 @@ Glow.leaveCall = async () => {
if (!activeCall) return;
Glow.state.calls.leaving = true;
Glow.calls.setStatusText("Leaving call…");
Glow.calls.setStatusText("Leaving call…");
try {
Glow.calls.sendSignal({ type: "hangup" });
@ -1199,7 +1376,7 @@ Glow.endCallForEveryone = async () => {
if (!values) return;
try {
Glow.calls.setStatusText("Ending call…");
Glow.calls.setStatusText("Ending call…");
await Glow.apiEndGroupCall(activeCall.groupSession.id);
Glow.showToast("Call ended.");
await Glow.endLocalCallUi();
@ -1331,7 +1508,7 @@ Glow.calls.openIncomingInviteDialog = async invite => {
}
Glow.state.calls.joining = true;
Glow.calls.setStatusText("Joining call…");
Glow.calls.setStatusText("Joining call…");
const session = await Glow.calls.acceptInvite(invite.id);
@ -1356,16 +1533,17 @@ Glow.calls.openIncomingInviteDialog = async invite => {
Glow.state.calls.panelOpen = true;
Glow.openCallPanel();
Glow.calls.setStatusText("Connecting microphone…");
Glow.calls.setStatusText("Connecting microphone…");
await Glow.calls.ensureLocalAudio();
if (session.group_session?.id) {
Glow.calls.setStatusText("Joining call…");
Glow.calls.setStatusText("Joining call…");
await Glow.apiJoinGroupCall(session.group_session.id);
}
await Glow.calls.connectWebSocket();
Glow.calls.startParticipantSync();
Glow.calls.startHeartbeat();
Glow.state.calls.joining = false;
Glow.calls.updateSummaryMetrics();
Glow.renderCallUi();
Glow.showToast("Joined group call.");
} catch (error) {
@ -1429,3 +1607,29 @@ Glow.els.endCallButton?.addEventListener("click", () => {
Glow.els.addToCallButton?.addEventListener("click", () => {
Glow.openAddToCallDialog?.();
});
Glow.els.callDragHandle?.addEventListener("mousedown", event => {
event.preventDefault();
Glow.calls.beginDrag(event);
});
document.addEventListener("mousemove", event => {
Glow.calls.onDrag?.(event);
});
document.addEventListener("mouseup", () => {
Glow.calls.endDrag?.();
});
window.addEventListener("resize", () => {
if (Glow.state.calls?.panelOpen) {
Glow.calls.applyPanelPosition?.();
}
});
Glow.calls.loadPanelPosition?.();
setInterval(() => {
if (Glow.state.calls?.activeCall) {
Glow.calls.updateSummaryMetrics?.();
}
}, 1000);