import { acceptIncomingCall, declineIncomingCall, hangupActiveCall, setMicMuted, subscribeCallState, toggleSpeakerMode, } from './call-service.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 speakerBtn = null; let muteBtn = 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 barHangupBtn = null; let unbind = null; let tickerId = 0; let currentSnapshot = null; let minimized = false; 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)); return `Звонок с ${peer} · ${elapsed}`; } function stopEvent(event) { event.preventDefault(); event.stopPropagation(); } function getIconSvg(name) { if (name === 'mic-on') { return ''; } if (name === 'mic-off') { return ''; } if (name === 'hangup') { return ''; } if (name === 'minimize') { return ''; } if (name === 'speaker-on') { return ''; } if (name === 'speaker-off') { return ''; } 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); } 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(); } 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 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); 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, 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('h2'); 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'; const controls = document.createElement('div'); controls.className = 'call-overlay-controls'; 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); 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); controls.append(speakerBtn, muteBtn, acceptBtn, declineBtn, hangupBtn); headerEl.append(titleEl, minimizeBtn); panelEl.append(headerEl, statusEl, controls); overlayEl.append(panelEl); rootEl.append(barEl, overlayEl); resolveAppShell().append(rootEl); } function applyExpandedState(snapshot) { overlayEl.hidden = false; barEl.hidden = true; titleEl.textContent = `Звонок: ${snapshot.peerLogin || 'пользователь'}`; statusEl.textContent = snapshot.statusText || ''; const incomingStage = Boolean(snapshot.canAnswer || snapshot.canDecline); const muted = Boolean(snapshot.muted); updateMuteButtons(muted); updateSpeakerButtons(Boolean(snapshot.speakerEnabled)); speakerBtn.hidden = incomingStage || !snapshot.canToggleSpeaker; speakerBtn.disabled = !snapshot.canToggleSpeaker; muteBtn.hidden = incomingStage || !snapshot.canMute; muteBtn.disabled = !snapshot.canMute; acceptBtn.hidden = !snapshot.canAnswer; declineBtn.hidden = !snapshot.canDecline; hangupBtn.hidden = incomingStage || !snapshot.canHangup; } function applyMinimizedState(snapshot) { overlayEl.hidden = true; barEl.hidden = false; barTextEl.textContent = buildBarText(snapshot); const muted = Boolean(snapshot.muted); updateMuteButtons(muted); updateSpeakerButtons(Boolean(snapshot.speakerEnabled)); barSpeakerBtn.hidden = !snapshot.canToggleSpeaker; barSpeakerBtn.disabled = !snapshot.canToggleSpeaker; barMuteBtn.hidden = !snapshot.canMute; barMuteBtn.disabled = !snapshot.canMute; const canEnd = snapshot.canHangup || snapshot.canDecline; barHangupBtn.hidden = !canEnd; barHangupBtn.disabled = !canEnd; setButtonIcon(barHangupBtn, 'hangup', snapshot.canDecline && !snapshot.canHangup ? 'Отклонить звонок' : 'Положить трубку'); } 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); }