Files
SHiNE-server/shine-UI/js/services/call-ui-service.js

616 lines
23 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
acceptIncomingCall,
declineIncomingCall,
hangupActiveCall,
setCameraEnabled,
setMicMuted,
subscribeCallState,
toggleSpeakerMode,
} from './call-service.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { loadProfileSnapshot } from './user-profile-params.js';
const MINIMIZED_BAR_HEIGHT_PX = 44;
let rootEl = null;
let overlayEl = null;
let panelEl = null;
let headerEl = null;
let titleEl = null;
let statusEl = null;
let mediaStageEl = null;
let remoteVideoEl = null;
let localPreviewWrapEl = null;
let localVideoEl = null;
let heroEl = null;
let avatarSlotEl = null;
let peerLoginEl = null;
let kindEl = null;
let topStatusEl = null;
let incomingActionsEl = null;
let activeActionsEl = null;
let speakerBtn = null;
let muteBtn = null;
let cameraBtn = null;
let acceptBtn = null;
let declineBtn = null;
let hangupBtn = null;
let minimizeBtn = null;
let barEl = null;
let barTextEl = null;
let barActionsEl = null;
let barSpeakerBtn = null;
let barMuteBtn = null;
let barCameraBtn = null;
let barHangupBtn = null;
let unbind = null;
let tickerId = 0;
let currentSnapshot = null;
let minimized = false;
let activeAvatarLogin = '';
let activeAvatarLoadToken = 0;
let localPreviewDragPointerId = null;
let localPreviewDragRect = null;
let localPreviewDragOffsetX = 0;
let localPreviewDragOffsetY = 0;
let localPreviewPosition = null;
const avatarSnapshotCache = new Map();
const avatarPendingByLogin = new Map();
function resolveAppShell() {
return document.querySelector('.app-shell') || document.body;
}
function pad2(value) {
return String(Math.max(0, Number(value) || 0)).padStart(2, '0');
}
function formatElapsedMs(ms) {
const totalSec = Math.max(0, Math.floor(Number(ms || 0) / 1000));
const hours = Math.floor(totalSec / 3600);
const minutes = Math.floor((totalSec % 3600) / 60);
const seconds = totalSec % 60;
if (hours > 0) return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
return `${pad2(minutes)}:${pad2(seconds)}`;
}
function getSnapshotElapsedMs(snapshot) {
const baseMs = Number(snapshot?.connectedAtMs || snapshot?.startedAtMs || 0);
if (!baseMs || !Number.isFinite(baseMs)) return 0;
return Math.max(0, Date.now() - baseMs);
}
function buildBarText(snapshot) {
const peer = String(snapshot?.peerLogin || '').trim() || 'пользователь';
const elapsed = formatElapsedMs(getSnapshotElapsedMs(snapshot));
const kind = String(snapshot?.titleText || 'Звонок').trim() || 'Звонок';
return `${kind} с ${peer} · ${elapsed}`;
}
async function loadAvatarSnapshot(login) {
const cleanLogin = String(login || '').trim();
if (!cleanLogin) return null;
const key = cleanLogin.toLowerCase();
if (avatarSnapshotCache.has(key)) return avatarSnapshotCache.get(key);
if (avatarPendingByLogin.has(key)) return avatarPendingByLogin.get(key);
const pending = loadProfileSnapshot(cleanLogin)
.then((snapshot) => {
avatarSnapshotCache.set(key, snapshot || null);
avatarPendingByLogin.delete(key);
return snapshot || null;
})
.catch(() => {
avatarSnapshotCache.set(key, null);
avatarPendingByLogin.delete(key);
return null;
});
avatarPendingByLogin.set(key, pending);
return pending;
}
function renderPeerAvatar(login, avatar = null) {
const hasAvatar = Boolean(String(avatar?.ar || '').trim());
const avatarEl = renderUserAvatar({
login: String(login || '').trim() || 'unknown',
avatar,
size: 'xl',
className: 'call-peer-avatar',
title: login ? `Профиль ${login}` : 'Профиль собеседника',
});
if (!hasAvatar) {
avatarEl.classList.add('call-peer-avatar--empty');
const fallbackEl = avatarEl.querySelector('.avatar-fallback');
if (fallbackEl) fallbackEl.textContent = '';
}
return avatarEl;
}
function updatePeerAvatar(login) {
if (!avatarSlotEl) return;
const cleanLogin = String(login || '').trim();
if (activeAvatarLogin === cleanLogin && avatarSlotEl.childElementCount > 0) return;
activeAvatarLogin = cleanLogin;
const loadToken = ++activeAvatarLoadToken;
avatarSlotEl.innerHTML = '';
avatarSlotEl.append(renderPeerAvatar(cleanLogin));
if (!cleanLogin) return;
void loadAvatarSnapshot(cleanLogin).then((snapshot) => {
if (!avatarSlotEl || loadToken !== activeAvatarLoadToken || activeAvatarLogin !== cleanLogin) return;
const avatar = snapshot?.avatar?.txId
? {
ar: String(snapshot.avatar.txId || '').trim(),
sha256Hex: String(snapshot.avatar.sha256Hex || '').trim().toLowerCase(),
}
: null;
const upgraded = renderPeerAvatar(cleanLogin, avatar);
avatarSlotEl.replaceChildren(upgraded);
});
}
function stopEvent(event) {
event.preventDefault();
event.stopPropagation();
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function getIconSvg(name) {
if (name === 'mic-on') {
return '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="3.5" width="6" height="11" rx="3" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M6 11.5a6 6 0 0 0 12 0" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 17.5v3" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M8.5 20.5h7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>';
}
if (name === 'mic-off') {
return '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="3.5" width="6" height="11" rx="3" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M6 11.5a6 6 0 0 0 7.2 5.88" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 17.5v3" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M8.5 20.5h7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M5 5l14 14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>';
}
if (name === 'hangup') {
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7l10 10M17 7 7 17" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>';
}
if (name === 'minimize') {
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 6v12M7.5 10.5 12 6l4.5 4.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
}
if (name === 'speaker-on') {
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 14h4l5 4V6L8 10H4z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/><path d="M16 9.5a4.5 4.5 0 0 1 0 5" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M18.8 7a8 8 0 0 1 0 10" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>';
}
if (name === 'speaker-off') {
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 10H4v4h4l5 4V6z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/><path d="M16 8l4 8" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M20 8l-4 8" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>';
}
if (name === 'camera-on') {
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8.5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M16 10.2 20 8v8l-4-2.2z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>';
}
if (name === 'camera-off') {
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8.5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M16 10.2 20 8v8l-4-2.2z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M5 5l14 14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>';
}
return '';
}
function setButtonIcon(button, iconName, label) {
if (!button) return;
button.innerHTML = getIconSvg(iconName);
button.setAttribute('aria-label', label);
button.title = label;
}
function updateMuteButtons(muted) {
const iconName = muted ? 'mic-off' : 'mic-on';
const label = muted ? 'Включить микрофон' : 'Выключить микрофон';
setButtonIcon(muteBtn, iconName, label);
setButtonIcon(barMuteBtn, iconName, label);
if (muteBtn) muteBtn.dataset.muted = muted ? '1' : '0';
if (barMuteBtn) barMuteBtn.dataset.muted = muted ? '1' : '0';
}
function updateSpeakerButtons(enabled) {
const iconName = enabled ? 'speaker-on' : 'speaker-off';
const label = enabled ? 'Выключить громкую связь' : 'Включить громкую связь';
setButtonIcon(speakerBtn, iconName, label);
setButtonIcon(barSpeakerBtn, iconName, label);
}
function updateCameraButtons(enabled) {
const iconName = enabled ? 'camera-on' : 'camera-off';
const label = enabled ? 'Выключить камеру' : 'Включить камеру';
setButtonIcon(cameraBtn, iconName, label);
setButtonIcon(barCameraBtn, iconName, label);
if (cameraBtn) cameraBtn.dataset.enabled = enabled ? '1' : '0';
if (barCameraBtn) barCameraBtn.dataset.enabled = enabled ? '1' : '0';
}
async function handleMuteClick(event) {
stopEvent(event);
const nowMuted = event.currentTarget?.dataset?.muted === '1';
await setMicMuted(!nowMuted);
}
async function handleEndCallClick(event) {
stopEvent(event);
if (!currentSnapshot) return;
if (currentSnapshot.canDecline && !currentSnapshot.canHangup) {
await declineIncomingCall();
return;
}
await hangupActiveCall();
}
async function handleSpeakerClick(event) {
stopEvent(event);
await toggleSpeakerMode();
}
async function handleCameraClick(event) {
stopEvent(event);
const enabled = event.currentTarget?.dataset?.enabled === '1';
try {
await setCameraEnabled(!enabled);
} catch {}
}
function setMinimized(nextValue) {
minimized = Boolean(nextValue);
render();
}
function ensureTicker() {
const shouldTick = Boolean(currentSnapshot);
if (shouldTick && !tickerId) {
tickerId = window.setInterval(() => {
if (!currentSnapshot) return;
render();
}, 1000);
}
if (!shouldTick && tickerId) {
window.clearInterval(tickerId);
tickerId = 0;
}
}
function applyLocalPreviewPosition() {
if (!localPreviewWrapEl) return;
if (!localPreviewPosition) {
localPreviewWrapEl.style.left = '';
localPreviewWrapEl.style.top = '';
localPreviewWrapEl.style.right = '';
localPreviewWrapEl.style.bottom = '';
return;
}
localPreviewWrapEl.style.left = `${localPreviewPosition.leftPx}px`;
localPreviewWrapEl.style.top = `${localPreviewPosition.topPx}px`;
localPreviewWrapEl.style.right = 'auto';
localPreviewWrapEl.style.bottom = 'auto';
}
function updateLocalPreviewPositionFromPointer(clientX, clientY) {
if (!mediaStageEl || !localPreviewWrapEl || !localPreviewDragRect) return;
const stageRect = mediaStageEl.getBoundingClientRect();
const width = localPreviewDragRect.width;
const height = localPreviewDragRect.height;
const nextLeft = clamp(clientX - stageRect.left - localPreviewDragOffsetX, 8, Math.max(8, stageRect.width - width - 8));
const nextTop = clamp(clientY - stageRect.top - localPreviewDragOffsetY, 8, Math.max(8, stageRect.height - height - 8));
localPreviewPosition = {
leftPx: Math.round(nextLeft),
topPx: Math.round(nextTop),
};
applyLocalPreviewPosition();
}
function handleLocalPreviewPointerDown(event) {
if (!localPreviewWrapEl || event.pointerType === 'mouse' && event.button !== 0) return;
localPreviewDragPointerId = event.pointerId;
localPreviewDragRect = localPreviewWrapEl.getBoundingClientRect();
localPreviewDragOffsetX = event.clientX - localPreviewDragRect.left;
localPreviewDragOffsetY = event.clientY - localPreviewDragRect.top;
try { localPreviewWrapEl.setPointerCapture(event.pointerId); } catch {}
stopEvent(event);
}
function handleLocalPreviewPointerMove(event) {
if (localPreviewDragPointerId !== event.pointerId) return;
updateLocalPreviewPositionFromPointer(event.clientX, event.clientY);
stopEvent(event);
}
function stopLocalPreviewDrag(event = null) {
if (event && localPreviewDragPointerId !== event.pointerId) return;
if (event && localPreviewWrapEl) {
try { localPreviewWrapEl.releasePointerCapture(event.pointerId); } catch {}
}
localPreviewDragPointerId = null;
localPreviewDragRect = null;
}
function ensureUi() {
if (rootEl) return;
rootEl = document.createElement('section');
rootEl.className = 'call-ui-root';
rootEl.hidden = true;
barEl = document.createElement('div');
barEl.className = 'call-minimized-bar';
barEl.hidden = true;
barTextEl = document.createElement('button');
barTextEl.type = 'button';
barTextEl.className = 'call-minimized-bar-text';
barTextEl.addEventListener('click', () => {
if (!currentSnapshot) return;
setMinimized(false);
});
barActionsEl = document.createElement('div');
barActionsEl.className = 'call-minimized-bar-actions';
barSpeakerBtn = document.createElement('button');
barSpeakerBtn.type = 'button';
barSpeakerBtn.className = 'call-icon-btn call-icon-btn--bar call-icon-btn--secondary';
barSpeakerBtn.addEventListener('click', handleSpeakerClick);
barMuteBtn = document.createElement('button');
barMuteBtn.type = 'button';
barMuteBtn.className = 'call-icon-btn call-icon-btn--bar call-icon-btn--secondary';
barMuteBtn.addEventListener('click', handleMuteClick);
barCameraBtn = document.createElement('button');
barCameraBtn.type = 'button';
barCameraBtn.className = 'call-icon-btn call-icon-btn--bar call-icon-btn--secondary';
barCameraBtn.addEventListener('click', handleCameraClick);
barHangupBtn = document.createElement('button');
barHangupBtn.type = 'button';
barHangupBtn.className = 'call-icon-btn call-icon-btn--bar call-icon-btn--danger';
barHangupBtn.addEventListener('click', handleEndCallClick);
barActionsEl.append(barSpeakerBtn, barMuteBtn, barCameraBtn, barHangupBtn);
barEl.append(barTextEl, barActionsEl);
overlayEl = document.createElement('div');
overlayEl.className = 'call-overlay';
overlayEl.hidden = true;
panelEl = document.createElement('div');
panelEl.className = 'call-overlay-panel';
headerEl = document.createElement('div');
headerEl.className = 'call-overlay-head';
titleEl = document.createElement('div');
titleEl.className = 'call-overlay-title';
minimizeBtn = document.createElement('button');
minimizeBtn.type = 'button';
minimizeBtn.className = 'call-icon-btn call-icon-btn--secondary call-overlay-minimize-btn';
setButtonIcon(minimizeBtn, 'minimize', 'Свернуть звонок');
minimizeBtn.addEventListener('click', (event) => {
stopEvent(event);
if (!currentSnapshot) return;
setMinimized(true);
});
statusEl = document.createElement('div');
statusEl.className = 'call-overlay-status';
mediaStageEl = document.createElement('div');
mediaStageEl.className = 'call-video-stage';
remoteVideoEl = document.createElement('video');
remoteVideoEl.className = 'call-remote-video';
remoteVideoEl.autoplay = true;
remoteVideoEl.playsInline = true;
remoteVideoEl.muted = true;
localPreviewWrapEl = document.createElement('div');
localPreviewWrapEl.className = 'call-local-preview';
localPreviewWrapEl.addEventListener('pointerdown', handleLocalPreviewPointerDown);
localPreviewWrapEl.addEventListener('pointermove', handleLocalPreviewPointerMove);
localPreviewWrapEl.addEventListener('pointerup', stopLocalPreviewDrag);
localPreviewWrapEl.addEventListener('pointercancel', stopLocalPreviewDrag);
localVideoEl = document.createElement('video');
localVideoEl.className = 'call-local-video';
localVideoEl.autoplay = true;
localVideoEl.playsInline = true;
localVideoEl.muted = true;
heroEl = document.createElement('div');
heroEl.className = 'call-hero';
avatarSlotEl = document.createElement('div');
avatarSlotEl.className = 'call-peer-avatar-slot';
peerLoginEl = document.createElement('div');
peerLoginEl.className = 'call-peer-login';
kindEl = document.createElement('div');
kindEl.className = 'call-kind';
topStatusEl = document.createElement('div');
topStatusEl.className = 'call-hero-status';
heroEl.append(avatarSlotEl, peerLoginEl, kindEl, topStatusEl);
localPreviewWrapEl.append(localVideoEl);
mediaStageEl.append(remoteVideoEl, heroEl, localPreviewWrapEl);
incomingActionsEl = document.createElement('div');
incomingActionsEl.className = 'call-overlay-controls call-overlay-controls--incoming';
activeActionsEl = document.createElement('div');
activeActionsEl.className = 'call-overlay-controls call-overlay-controls--active';
speakerBtn = document.createElement('button');
speakerBtn.type = 'button';
speakerBtn.className = 'call-icon-btn call-icon-btn--secondary';
speakerBtn.addEventListener('click', handleSpeakerClick);
muteBtn = document.createElement('button');
muteBtn.type = 'button';
muteBtn.className = 'call-icon-btn call-icon-btn--secondary';
muteBtn.addEventListener('click', handleMuteClick);
cameraBtn = document.createElement('button');
cameraBtn.type = 'button';
cameraBtn.className = 'call-icon-btn call-icon-btn--secondary';
cameraBtn.addEventListener('click', handleCameraClick);
acceptBtn = document.createElement('button');
acceptBtn.type = 'button';
acceptBtn.className = 'call-accept-btn';
acceptBtn.textContent = 'Ответить';
acceptBtn.addEventListener('click', async () => {
await acceptIncomingCall();
});
declineBtn = document.createElement('button');
declineBtn.type = 'button';
declineBtn.className = 'destructive-btn call-decline-btn';
declineBtn.textContent = 'Отклонить';
declineBtn.addEventListener('click', async () => {
await declineIncomingCall();
});
hangupBtn = document.createElement('button');
hangupBtn.type = 'button';
hangupBtn.className = 'call-icon-btn call-icon-btn--danger';
setButtonIcon(hangupBtn, 'hangup', 'Положить трубку');
hangupBtn.addEventListener('click', handleEndCallClick);
incomingActionsEl.append(acceptBtn, declineBtn);
activeActionsEl.append(speakerBtn, muteBtn, cameraBtn, hangupBtn);
headerEl.append(titleEl, minimizeBtn);
panelEl.append(mediaStageEl, headerEl, statusEl, incomingActionsEl, activeActionsEl);
overlayEl.append(panelEl);
rootEl.append(barEl, overlayEl);
resolveAppShell().append(rootEl);
}
function applyExpandedState(snapshot) {
overlayEl.hidden = false;
barEl.hidden = true;
titleEl.textContent = '';
statusEl.textContent = '';
peerLoginEl.textContent = snapshot.peerLogin || 'пользователь';
kindEl.textContent = snapshot.titleText || 'Звонок';
topStatusEl.textContent = snapshot.statusText || '';
updatePeerAvatar(snapshot.peerLogin || '');
const incomingStage = Boolean(snapshot.canAnswer || snapshot.canDecline);
const muted = Boolean(snapshot.muted);
updateMuteButtons(muted);
updateSpeakerButtons(Boolean(snapshot.speakerEnabled));
updateCameraButtons(Boolean(snapshot.cameraEnabled));
panelEl.classList.toggle('call-overlay-panel--remote-video', Boolean(snapshot.hasRemoteVideo));
mediaStageEl.classList.toggle('call-video-stage--remote-video', Boolean(snapshot.hasRemoteVideo));
heroEl.hidden = Boolean(snapshot.hasRemoteVideo);
minimizeBtn.hidden = incomingStage;
minimizeBtn.disabled = incomingStage;
incomingActionsEl.hidden = !incomingStage;
activeActionsEl.hidden = incomingStage;
speakerBtn.hidden = !snapshot.canToggleSpeaker;
speakerBtn.disabled = !snapshot.canToggleSpeaker;
muteBtn.hidden = !snapshot.canMute;
muteBtn.disabled = !snapshot.canMute;
cameraBtn.hidden = !snapshot.canToggleCamera;
cameraBtn.disabled = !snapshot.canToggleCamera;
acceptBtn.hidden = !incomingStage || !snapshot.canAnswer;
declineBtn.hidden = !incomingStage || !snapshot.canDecline;
hangupBtn.hidden = !snapshot.canHangup;
mediaStageEl.hidden = false;
const remoteStream = snapshot.remoteMediaStream || null;
const localStream = snapshot.localPreviewStream || null;
const remoteVideoSource = snapshot.hasRemoteVideo ? remoteStream : null;
if (remoteVideoEl.srcObject !== remoteVideoSource) remoteVideoEl.srcObject = remoteVideoSource;
if (localVideoEl.srcObject !== localStream) localVideoEl.srcObject = localStream;
remoteVideoEl.hidden = !snapshot.hasRemoteVideo;
localPreviewWrapEl.hidden = !snapshot.hasLocalVideo;
if (!snapshot.hasLocalVideo) {
localPreviewPosition = null;
applyLocalPreviewPosition();
}
}
function applyMinimizedState(snapshot) {
overlayEl.hidden = true;
barEl.hidden = false;
barTextEl.textContent = buildBarText(snapshot);
const incomingStage = Boolean(snapshot.canAnswer || snapshot.canDecline);
const muted = Boolean(snapshot.muted);
updateMuteButtons(muted);
updateSpeakerButtons(Boolean(snapshot.speakerEnabled));
updateCameraButtons(Boolean(snapshot.cameraEnabled));
barSpeakerBtn.hidden = incomingStage || !snapshot.canToggleSpeaker;
barSpeakerBtn.disabled = !snapshot.canToggleSpeaker;
barMuteBtn.hidden = incomingStage || !snapshot.canMute;
barMuteBtn.disabled = !snapshot.canMute;
barCameraBtn.hidden = incomingStage || !snapshot.canToggleCamera;
barCameraBtn.disabled = !snapshot.canToggleCamera;
const canEnd = !incomingStage && snapshot.canHangup;
barHangupBtn.hidden = !canEnd;
barHangupBtn.disabled = !canEnd;
setButtonIcon(barHangupBtn, 'hangup', 'Положить трубку');
barActionsEl.hidden = incomingStage || (!snapshot.canToggleSpeaker && !snapshot.canMute && !snapshot.canToggleCamera && !canEnd);
}
function syncShellState() {
const appShell = resolveAppShell();
if (!appShell) return;
if (currentSnapshot && minimized) {
appShell.classList.add('has-minimized-call');
const measuredHeight = Math.max(
MINIMIZED_BAR_HEIGHT_PX,
Math.ceil(Number(barEl?.offsetHeight || 0)),
);
appShell.style.setProperty('--call-minimized-bar-height', `${measuredHeight}px`);
} else {
appShell.classList.remove('has-minimized-call');
appShell.style.removeProperty('--call-minimized-bar-height');
}
}
function render() {
ensureUi();
rootEl.hidden = !currentSnapshot;
ensureTicker();
if (!currentSnapshot) {
syncShellState();
overlayEl.hidden = true;
barEl.hidden = true;
return;
}
if (minimized) {
applyMinimizedState(currentSnapshot);
syncShellState();
return;
}
applyExpandedState(currentSnapshot);
syncShellState();
}
function applyCallState(snapshot) {
ensureUi();
currentSnapshot = snapshot || null;
if (!currentSnapshot) {
minimized = false;
}
render();
}
export function initCallUiOverlay() {
ensureUi();
if (unbind) {
unbind();
unbind = null;
}
unbind = subscribeCallState(applyCallState);
}