735 lines
19 KiB
JavaScript
735 lines
19 KiB
JavaScript
// static/js/12-calls.js
|
|
"use strict";
|
|
window.Glow = window.Glow || {};
|
|
Glow.calls = Glow.calls || {};
|
|
|
|
Glow.calls.state = () => Glow.state.calls;
|
|
|
|
Glow.calls.ensureState = () => {
|
|
const state = Glow.calls.state();
|
|
state.activeCall ||= null;
|
|
state.ws ||= null;
|
|
state.session ||= null;
|
|
state.localStream ||= null;
|
|
state.muted ||= false;
|
|
state.selfPeerId ||= null;
|
|
state.peerConnections ||= new Map();
|
|
state.remoteStreams ||= new Map();
|
|
state.peers ||= [];
|
|
state.reconnectTimer ||= null;
|
|
state.invites ||= [];
|
|
state.invitePollTimer ||= null;
|
|
};
|
|
|
|
Glow.calls.scopeLabel = invite => {
|
|
if (!invite) return "call";
|
|
if (invite.scope_kind === "channel") {
|
|
return `channel ${invite.scope_slug}`;
|
|
}
|
|
return `room ${invite.scope_slug}`;
|
|
};
|
|
|
|
Glow.calls.getCurrentScope = () => {
|
|
if (Glow.state.activeTeam && Glow.state.activeChannel) {
|
|
return {
|
|
scope_kind: "channel",
|
|
team_slug: Glow.state.activeTeam.slug,
|
|
channel_slug: Glow.state.activeChannel.slug,
|
|
label: `# ${Glow.state.activeChannel.name}`
|
|
};
|
|
}
|
|
if (Glow.state.activeRoom) {
|
|
return {
|
|
scope_kind: "room",
|
|
room_slug: Glow.state.activeRoom.slug,
|
|
label: Glow.state.activeRoom.name
|
|
};
|
|
}
|
|
return null;
|
|
};
|
|
|
|
Glow.calls.isGroupCallContext = () => {
|
|
return Boolean(Glow.calls.getCurrentScope());
|
|
};
|
|
|
|
Glow.calls.wsReady = () => {
|
|
const ws = Glow.calls.state().ws;
|
|
return Boolean(ws && ws.readyState === WebSocket.OPEN);
|
|
};
|
|
|
|
Glow.calls.sendSignal = payload => {
|
|
const ws = Glow.calls.state().ws;
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
ws.send(JSON.stringify(payload));
|
|
return true;
|
|
};
|
|
|
|
Glow.calls.replaceHttpWs = url =>
|
|
url.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
|
|
|
|
Glow.calls.setRemoteAudioFromAnyPeer = () => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
const firstStream = [...state.remoteStreams.values()][0] || null;
|
|
if (Glow.els.remoteAudio) {
|
|
Glow.els.remoteAudio.srcObject = firstStream;
|
|
}
|
|
};
|
|
|
|
Glow.calls.upsertPeer = peer => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
const peers = state.peers || [];
|
|
const participantId =
|
|
peer.participantId || peer.peer_id || peer.peerId || peer.id;
|
|
if (!participantId) return;
|
|
|
|
const normalized = {
|
|
participantId,
|
|
roomId: peer.roomId || peer.room_id || state.session?.call_id || "",
|
|
username: peer.username || participantId
|
|
};
|
|
|
|
const index = peers.findIndex(item => item.participantId === participantId);
|
|
if (index >= 0) {
|
|
peers[index] = { ...peers[index], ...normalized };
|
|
} else {
|
|
peers.push(normalized);
|
|
}
|
|
state.peers = peers;
|
|
};
|
|
|
|
Glow.calls.removePeerConnection = peerId => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
|
|
const pc = state.peerConnections.get(peerId);
|
|
if (pc) {
|
|
try {
|
|
pc.close();
|
|
} catch (_) {}
|
|
}
|
|
|
|
state.peerConnections.delete(peerId);
|
|
state.remoteStreams.delete(peerId);
|
|
Glow.calls.setRemoteAudioFromAnyPeer();
|
|
};
|
|
|
|
Glow.calls.removePeer = peerId => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
state.peers = (state.peers || []).filter(peer => peer.participantId !== peerId);
|
|
Glow.calls.removePeerConnection(peerId);
|
|
};
|
|
|
|
Glow.calls.listOtherPeers = () => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
return (state.peers || []).filter(peer => peer.participantId !== state.selfPeerId);
|
|
};
|
|
|
|
Glow.calls.ensureLocalAudio = async () => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
if (state.localStream) return state.localStream;
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
audio: true,
|
|
video: false
|
|
});
|
|
state.localStream = stream;
|
|
return stream;
|
|
};
|
|
|
|
Glow.calls.ensurePeerConnection = async peerId => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
|
|
if (state.peerConnections.has(peerId)) {
|
|
return state.peerConnections.get(peerId);
|
|
}
|
|
|
|
const pc = new RTCPeerConnection({
|
|
iceServers: state.session?.iceServers || []
|
|
});
|
|
|
|
pc.onicecandidate = event => {
|
|
if (!event.candidate) return;
|
|
Glow.calls.sendSignal({
|
|
type: "ice",
|
|
to: peerId,
|
|
candidate: event.candidate.candidate,
|
|
sdpMid: event.candidate.sdpMid,
|
|
sdpMLineIndex: event.candidate.sdpMLineIndex
|
|
});
|
|
};
|
|
|
|
pc.ontrack = event => {
|
|
const stream = event.streams?.[0];
|
|
if (!stream) return;
|
|
state.remoteStreams.set(peerId, stream);
|
|
Glow.calls.setRemoteAudioFromAnyPeer();
|
|
};
|
|
|
|
pc.onconnectionstatechange = () => {
|
|
const bad =
|
|
pc.connectionState === "failed" ||
|
|
pc.connectionState === "disconnected";
|
|
if (bad) {
|
|
Glow.showToast(`Connection issue with ${peerId}.`, "error");
|
|
}
|
|
};
|
|
|
|
state.peerConnections.set(peerId, pc);
|
|
|
|
const stream = await Glow.calls.ensureLocalAudio();
|
|
const existingTracks = pc.getSenders().map(sender => sender.track).filter(Boolean);
|
|
|
|
for (const track of stream.getTracks()) {
|
|
if (!existingTracks.includes(track)) {
|
|
pc.addTrack(track, stream);
|
|
}
|
|
}
|
|
|
|
return pc;
|
|
};
|
|
|
|
Glow.calls.makeOfferTo = async peerId => {
|
|
const pc = await Glow.calls.ensurePeerConnection(peerId);
|
|
const offer = await pc.createOffer();
|
|
await pc.setLocalDescription(offer);
|
|
Glow.calls.sendSignal({
|
|
type: "offer",
|
|
to: peerId,
|
|
sdp: offer.sdp
|
|
});
|
|
};
|
|
|
|
Glow.calls.callAllPeers = async () => {
|
|
const peers = Glow.calls.listOtherPeers();
|
|
if (!peers.length) {
|
|
Glow.showToast("No peers available yet.", "error");
|
|
return;
|
|
}
|
|
|
|
for (const peer of peers) {
|
|
try {
|
|
await Glow.calls.makeOfferTo(peer.participantId);
|
|
} catch (_) {}
|
|
}
|
|
};
|
|
|
|
Glow.calls.fetchGroupCallSession = async scope => {
|
|
const data = await Glow.apiRequest("/api/group-calls/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(scope)
|
|
});
|
|
|
|
const helperResp = await fetch(`${data.calls_base}/api/call/session`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ token: data.token })
|
|
});
|
|
|
|
let helperData = {};
|
|
try {
|
|
helperData = await helperResp.json();
|
|
} catch (_) {}
|
|
|
|
if (!helperResp.ok || !helperData.ok) {
|
|
throw new Error(helperData.error || `Call helper failed (${helperResp.status})`);
|
|
}
|
|
|
|
helperData.calls_base = data.calls_base;
|
|
helperData.group_session = data.session;
|
|
return helperData;
|
|
};
|
|
|
|
Glow.calls.connectWebSocket = async () => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
|
|
if (!state.session?.ws_url) {
|
|
throw new Error("Missing helper WebSocket URL.");
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const ws = new WebSocket(Glow.calls.replaceHttpWs(state.session.ws_url));
|
|
state.ws = ws;
|
|
|
|
ws.onopen = () => {
|
|
resolve();
|
|
};
|
|
|
|
ws.onerror = error => {
|
|
reject(error);
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
if (state.activeCall) {
|
|
Glow.calls.scheduleReconnect();
|
|
}
|
|
};
|
|
|
|
ws.onmessage = async event => {
|
|
let signal;
|
|
try {
|
|
signal = JSON.parse(event.data);
|
|
} catch (_) {
|
|
return;
|
|
}
|
|
|
|
if (signal.type === "joined") {
|
|
state.selfPeerId = signal.participantId;
|
|
Glow.renderCallUi();
|
|
return;
|
|
}
|
|
|
|
if (signal.type === "participant-joined") {
|
|
Glow.calls.upsertPeer({
|
|
participantId: signal.participantId,
|
|
roomId: signal.roomId
|
|
});
|
|
Glow.renderCallUi();
|
|
|
|
if (signal.participantId && signal.participantId !== state.selfPeerId) {
|
|
try {
|
|
await Glow.calls.makeOfferTo(signal.participantId);
|
|
} catch (_) {}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (signal.type === "participant-left") {
|
|
Glow.calls.removePeer(signal.participantId);
|
|
Glow.renderCallUi();
|
|
Glow.showToast("Peer left call.");
|
|
return;
|
|
}
|
|
|
|
if (signal.type === "peer-reconnected") {
|
|
Glow.showToast("Peer reconnected.");
|
|
return;
|
|
}
|
|
|
|
if (signal.type === "reoffer-needed") {
|
|
if (signal.participantId) {
|
|
try {
|
|
await Glow.calls.makeOfferTo(signal.participantId);
|
|
} catch (_) {}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (signal.type === "offer") {
|
|
const from = signal.from;
|
|
if (!from) return;
|
|
|
|
const pc = await Glow.calls.ensurePeerConnection(from);
|
|
await pc.setRemoteDescription({ type: "offer", sdp: signal.sdp });
|
|
const answer = await pc.createAnswer();
|
|
await pc.setLocalDescription(answer);
|
|
|
|
Glow.calls.sendSignal({
|
|
type: "answer",
|
|
to: from,
|
|
sdp: answer.sdp
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (signal.type === "answer") {
|
|
const from = signal.from;
|
|
if (!from) return;
|
|
const pc = await Glow.calls.ensurePeerConnection(from);
|
|
await pc.setRemoteDescription({ type: "answer", sdp: signal.sdp });
|
|
return;
|
|
}
|
|
|
|
if (signal.type === "ice" || signal.type === "ice-candidate") {
|
|
const from = signal.from;
|
|
if (!from) return;
|
|
const pc = await Glow.calls.ensurePeerConnection(from);
|
|
try {
|
|
await pc.addIceCandidate({
|
|
candidate: signal.candidate,
|
|
sdpMid: signal.sdpMid,
|
|
sdpMLineIndex: signal.sdpMLineIndex
|
|
});
|
|
} catch (_) {}
|
|
}
|
|
};
|
|
});
|
|
};
|
|
|
|
Glow.calls.resetTransportOnly = () => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
|
|
clearTimeout(state.reconnectTimer);
|
|
|
|
try {
|
|
if (state.ws) {
|
|
state.ws.onopen = null;
|
|
state.ws.onclose = null;
|
|
state.ws.onerror = null;
|
|
state.ws.onmessage = null;
|
|
state.ws.close();
|
|
}
|
|
} catch (_) {}
|
|
|
|
for (const [, pc] of state.peerConnections) {
|
|
try {
|
|
pc.onicecandidate = null;
|
|
pc.ontrack = null;
|
|
pc.onconnectionstatechange = null;
|
|
pc.close();
|
|
} catch (_) {}
|
|
}
|
|
|
|
state.ws = null;
|
|
state.peerConnections = new Map();
|
|
state.remoteStreams = new Map();
|
|
state.peers = [];
|
|
state.selfPeerId = null;
|
|
state.reconnectTimer = null;
|
|
};
|
|
|
|
Glow.calls.resetAll = () => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
|
|
Glow.calls.resetTransportOnly();
|
|
|
|
try {
|
|
if (state.localStream) {
|
|
for (const track of state.localStream.getTracks()) {
|
|
track.stop();
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
|
|
state.localStream = null;
|
|
state.activeCall = null;
|
|
state.session = null;
|
|
state.muted = false;
|
|
};
|
|
|
|
Glow.endLocalCallUi = async () => {
|
|
Glow.calls.resetAll();
|
|
if (Glow.els.remoteAudio) {
|
|
Glow.els.remoteAudio.srcObject = null;
|
|
}
|
|
Glow.renderCallUi();
|
|
};
|
|
|
|
Glow.calls.scheduleReconnect = () => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
|
|
clearTimeout(state.reconnectTimer);
|
|
state.reconnectTimer = setTimeout(async () => {
|
|
try {
|
|
if (!state.activeCall) return;
|
|
const scope = state.activeCall.scope;
|
|
if (!scope) return;
|
|
|
|
Glow.calls.resetTransportOnly();
|
|
state.session = await Glow.calls.fetchGroupCallSession(scope);
|
|
await Glow.calls.connectWebSocket();
|
|
Glow.showToast("Call reconnected.");
|
|
Glow.renderCallUi();
|
|
} catch (_) {
|
|
Glow.calls.scheduleReconnect();
|
|
}
|
|
}, 2000);
|
|
};
|
|
|
|
Glow.renderCallUi = () => {
|
|
Glow.calls.ensureState();
|
|
const state = Glow.calls.state();
|
|
const active = Boolean(state.activeCall);
|
|
const scope = state.activeCall?.scope || Glow.calls.getCurrentScope();
|
|
const peerCount = Glow.calls.listOtherPeers().length;
|
|
|
|
if (!Glow.els.callStatus) return;
|
|
|
|
Glow.els.callPeerLabel.textContent =
|
|
scope?.label ||
|
|
Glow.state.activeChannel?.name ||
|
|
Glow.state.activeRoom?.name ||
|
|
"No active call";
|
|
|
|
Glow.els.callStatus.textContent = active
|
|
? peerCount > 0
|
|
? `Group call active · ${peerCount} peer(s)`
|
|
: "Call open"
|
|
: "Idle";
|
|
|
|
Glow.els.startCallButton?.classList.toggle("hidden", !Glow.calls.isGroupCallContext());
|
|
Glow.els.answerCallButton?.classList.add("hidden");
|
|
Glow.els.muteCallButton?.classList.toggle("hidden", !active);
|
|
Glow.els.hangupCallButton?.classList.toggle("hidden", !active);
|
|
if (Glow.els.addToCallButton) {
|
|
Glow.els.addToCallButton.classList.toggle("hidden", !active);
|
|
}
|
|
|
|
Glow.els.muteCallButton.textContent =
|
|
state.muted ? "Unmute" : "Mute";
|
|
};
|
|
|
|
Glow.openCallPanel = () => {
|
|
Glow.els.callOverlay?.classList.remove("hidden");
|
|
Glow.renderCallUi();
|
|
};
|
|
|
|
Glow.startCall = async () => {
|
|
Glow.calls.ensureState();
|
|
|
|
const scope = Glow.calls.getCurrentScope();
|
|
if (!scope) {
|
|
Glow.showToast("Open a room or channel to start a group call.", "error");
|
|
return;
|
|
}
|
|
|
|
if (!navigator.mediaDevices?.getUserMedia || !window.RTCPeerConnection) {
|
|
Glow.showToast("Voice calls are not supported in this browser.", "error");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const session = await Glow.calls.fetchGroupCallSession(scope);
|
|
Glow.state.calls.session = session;
|
|
Glow.state.calls.activeCall = {
|
|
id: session.call_id,
|
|
active: true,
|
|
scope,
|
|
groupSession: session.group_session
|
|
};
|
|
Glow.state.calls.selfPeerId = session.peer_id;
|
|
Glow.state.calls.peers = [];
|
|
Glow.state.calls.peerConnections = new Map();
|
|
Glow.state.calls.remoteStreams = new Map();
|
|
|
|
Glow.openCallPanel();
|
|
await Glow.calls.ensureLocalAudio();
|
|
await Glow.calls.connectWebSocket();
|
|
Glow.renderCallUi();
|
|
Glow.showToast("Group call ready.");
|
|
} catch (error) {
|
|
Glow.showToast(error.message || "Could not start group call.", "error");
|
|
await Glow.endLocalCallUi();
|
|
}
|
|
};
|
|
|
|
Glow.toggleMuteCall = () => {
|
|
const stream = Glow.state.calls.localStream;
|
|
if (!stream) return;
|
|
Glow.state.calls.muted = !Glow.state.calls.muted;
|
|
for (const track of stream.getAudioTracks()) {
|
|
track.enabled = !Glow.state.calls.muted;
|
|
}
|
|
Glow.renderCallUi();
|
|
};
|
|
|
|
Glow.hangupCall = async () => {
|
|
try {
|
|
Glow.calls.sendSignal({ type: "hangup" });
|
|
} catch (_) {}
|
|
Glow.showToast("Call ended.");
|
|
await Glow.endLocalCallUi();
|
|
};
|
|
|
|
Glow.calls.requestInvite = async username => {
|
|
Glow.calls.ensureState();
|
|
const activeCall = Glow.state.calls.activeCall;
|
|
if (!activeCall?.groupSession?.id) {
|
|
throw new Error("No active group call session.");
|
|
}
|
|
|
|
return Glow.apiRequest("/api/group-calls/invite", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
group_call_session_id: activeCall.groupSession.id,
|
|
username
|
|
})
|
|
});
|
|
};
|
|
|
|
Glow.openAddToCallDialog = async () => {
|
|
if (!Glow.state.calls.activeCall?.groupSession?.id) {
|
|
Glow.showToast("Start a call first.", "error");
|
|
return;
|
|
}
|
|
|
|
const values = await Glow.openDialog({
|
|
title: "Add user to call",
|
|
body: "Send a stored invite to another user for this room/channel call.",
|
|
confirmText: "Send invite",
|
|
fields: [
|
|
{
|
|
name: "username",
|
|
label: "Username",
|
|
placeholder: "username",
|
|
maxLength: 30
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!values?.username) return;
|
|
|
|
try {
|
|
const result = await Glow.calls.requestInvite(values.username.trim().toLowerCase());
|
|
Glow.showToast(`Invite sent to ${result.invite.invitee_username}.`);
|
|
} catch (error) {
|
|
Glow.showToast(error.message || "Could not send invite.", "error");
|
|
}
|
|
};
|
|
|
|
Glow.calls.fetchInvites = async () => {
|
|
Glow.calls.ensureState();
|
|
if (!Glow.state.currentUser) return [];
|
|
const data = await Glow.apiRequest("/api/group-calls/invites");
|
|
Glow.state.calls.invites = data.invites || [];
|
|
return Glow.state.calls.invites;
|
|
};
|
|
|
|
Glow.calls.acceptInvite = async inviteId => {
|
|
const data = await Glow.apiRequest(`/api/group-calls/invites/${encodeURIComponent(inviteId)}/accept`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({})
|
|
});
|
|
|
|
const helperResp = await fetch(`${data.calls_base}/api/call/session`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ token: data.token })
|
|
});
|
|
|
|
let helperData = {};
|
|
try {
|
|
helperData = await helperResp.json();
|
|
} catch (_) {}
|
|
|
|
if (!helperResp.ok || !helperData.ok) {
|
|
throw new Error(helperData.error || `Call helper failed (${helperResp.status})`);
|
|
}
|
|
|
|
helperData.calls_base = data.calls_base;
|
|
helperData.group_session = {
|
|
helper_call_id: data.call_id
|
|
};
|
|
return helperData;
|
|
};
|
|
|
|
Glow.calls.dismissInvite = async inviteId => {
|
|
await Glow.apiRequest(`/api/group-calls/invites/${encodeURIComponent(inviteId)}/dismiss`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({})
|
|
});
|
|
};
|
|
|
|
Glow.calls.openIncomingInviteDialog = async invite => {
|
|
const values = await Glow.openDialog({
|
|
title: "Incoming group call invite",
|
|
body: `${invite.inviter_username} invited you to join ${Glow.calls.scopeLabel(invite)}.`,
|
|
confirmText: "Join call",
|
|
cancelText: "Dismiss"
|
|
});
|
|
|
|
if (!values) {
|
|
try {
|
|
await Glow.calls.dismissInvite(invite.id);
|
|
} catch (_) {}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const session = await Glow.calls.acceptInvite(invite.id);
|
|
|
|
Glow.state.calls.session = session;
|
|
Glow.state.calls.activeCall = {
|
|
id: session.call_id,
|
|
active: true,
|
|
scope: null,
|
|
groupSession: session.group_session
|
|
};
|
|
Glow.state.calls.selfPeerId = session.peer_id;
|
|
Glow.state.calls.peers = [];
|
|
Glow.state.calls.peerConnections = new Map();
|
|
Glow.state.calls.remoteStreams = new Map();
|
|
|
|
Glow.openCallPanel();
|
|
await Glow.calls.ensureLocalAudio();
|
|
await Glow.calls.connectWebSocket();
|
|
Glow.renderCallUi();
|
|
Glow.showToast("Joined group call.");
|
|
} catch (error) {
|
|
Glow.showToast(error.message || "Could not join group call.", "error");
|
|
}
|
|
};
|
|
|
|
Glow.calls.pollInvites = async () => {
|
|
Glow.calls.ensureState();
|
|
if (!Glow.state.currentUser || document.hidden) return;
|
|
|
|
try {
|
|
const invites = await Glow.calls.fetchInvites();
|
|
if (!invites.length) return;
|
|
|
|
const invite = invites[0];
|
|
await Glow.calls.openIncomingInviteDialog(invite);
|
|
} catch (_) {}
|
|
};
|
|
|
|
Glow.calls.scheduleInvitePolling = () => {
|
|
Glow.calls.ensureState();
|
|
clearTimeout(Glow.state.calls.invitePollTimer);
|
|
if (!Glow.state.currentUser) return;
|
|
|
|
Glow.state.calls.invitePollTimer = setTimeout(async () => {
|
|
await Glow.calls.pollInvites();
|
|
Glow.calls.scheduleInvitePolling();
|
|
}, document.hidden ? 12000 : 6000);
|
|
};
|
|
|
|
Glow.syncActiveCallForDm = async () => {
|
|
Glow.renderCallUi();
|
|
};
|
|
|
|
Glow.pollCallSignals = async () => {
|
|
};
|
|
|
|
Glow.scheduleCallPolling = () => {
|
|
};
|
|
|
|
Glow.answerIncomingCall = async () => {
|
|
};
|
|
|
|
Glow.els.startCallButton?.addEventListener("click", async () => {
|
|
if (!Glow.state.calls.activeCall) {
|
|
await Glow.startCall();
|
|
return;
|
|
}
|
|
await Glow.calls.callAllPeers();
|
|
});
|
|
|
|
Glow.els.answerCallButton?.addEventListener("click", () => {
|
|
Glow.answerIncomingCall?.();
|
|
});
|
|
|
|
Glow.els.muteCallButton?.addEventListener("click", () => {
|
|
Glow.toggleMuteCall?.();
|
|
});
|
|
|
|
Glow.els.hangupCallButton?.addEventListener("click", () => {
|
|
Glow.hangupCall?.();
|
|
});
|
|
|
|
Glow.els.addToCallButton?.addEventListener("click", () => {
|
|
Glow.openAddToCallDialog?.();
|
|
}); |