SHA256
UI: сворачивание активного звонка
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
||||
client.version=1.2.347
|
||||
client.version=1.2.348
|
||||
server.version=1.2.319
|
||||
|
||||
@@ -554,6 +554,8 @@ function getCallStateSnapshot() {
|
||||
direction: call.direction || 'out',
|
||||
phase: callPhase,
|
||||
statusText: call.statusText || '',
|
||||
startedAtMs: Number(call.startedAtMs || 0),
|
||||
connectedAtMs: Number(call.connectedAtMs || 0),
|
||||
muted: Boolean(call.muted),
|
||||
canAnswer: callPhase === 'incoming',
|
||||
canDecline: callPhase === 'incoming',
|
||||
@@ -658,6 +660,7 @@ function buildCallFactsLine(call, extra = {}) {
|
||||
pcSignalingState: pc?.signalingState || '',
|
||||
hasLocalStream: Boolean(call?.localStream),
|
||||
localAudioTracksCount: call?.localStream?.getAudioTracks?.()?.length || 0,
|
||||
localTrackStates: summarizeMediaSnapshot(buildMediaSnapshot(call)),
|
||||
timeline: buildTimelineSummary(call),
|
||||
...extra,
|
||||
};
|
||||
@@ -666,6 +669,60 @@ function buildCallFactsLine(call, extra = {}) {
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function buildMediaSnapshot(call) {
|
||||
const localStream = call?.localStream || null;
|
||||
const localTracks = localStream?.getTracks?.() || [];
|
||||
const remoteAudio = call?.remoteAudio || null;
|
||||
const remoteStream = remoteAudio?.srcObject || null;
|
||||
const remoteTracks = remoteStream?.getTracks?.() || [];
|
||||
return {
|
||||
hasLocalStream: Boolean(localStream),
|
||||
localTrackCount: localTracks.length,
|
||||
localAudioTracksCount: localStream?.getAudioTracks?.()?.length || 0,
|
||||
localTrackStates: localTracks.map((track) => ({
|
||||
kind: String(track?.kind || ''),
|
||||
enabled: Boolean(track?.enabled),
|
||||
muted: Boolean(track?.muted),
|
||||
readyState: String(track?.readyState || ''),
|
||||
label: String(track?.label || ''),
|
||||
})),
|
||||
hasRemoteAudio: Boolean(remoteAudio),
|
||||
hasRemoteAudioSrcObject: Boolean(remoteStream),
|
||||
remoteTrackCount: remoteTracks.length,
|
||||
remoteTrackStates: remoteTracks.map((track) => ({
|
||||
kind: String(track?.kind || ''),
|
||||
enabled: Boolean(track?.enabled),
|
||||
muted: Boolean(track?.muted),
|
||||
readyState: String(track?.readyState || ''),
|
||||
label: String(track?.label || ''),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeMediaSnapshot(snapshot) {
|
||||
if (!snapshot) return '';
|
||||
const localSummary = Array.isArray(snapshot.localTrackStates)
|
||||
? snapshot.localTrackStates.map((track) => (
|
||||
`${track.kind}:${track.readyState}:${track.enabled ? 'on' : 'off'}:${track.muted ? 'muted' : 'live'}`
|
||||
)).join('|')
|
||||
: '';
|
||||
const remoteSummary = Array.isArray(snapshot.remoteTrackStates)
|
||||
? snapshot.remoteTrackStates.map((track) => (
|
||||
`${track.kind}:${track.readyState}:${track.enabled ? 'on' : 'off'}:${track.muted ? 'muted' : 'live'}`
|
||||
)).join('|')
|
||||
: '';
|
||||
return [
|
||||
`localStream=${snapshot.hasLocalStream ? 1 : 0}`,
|
||||
`localTracks=${Number(snapshot.localTrackCount || 0)}`,
|
||||
`localAudio=${Number(snapshot.localAudioTracksCount || 0)}`,
|
||||
`localStates=${localSummary || '-'}`,
|
||||
`remoteAudio=${snapshot.hasRemoteAudio ? 1 : 0}`,
|
||||
`remoteSrc=${snapshot.hasRemoteAudioSrcObject ? 1 : 0}`,
|
||||
`remoteTracks=${Number(snapshot.remoteTrackCount || 0)}`,
|
||||
`remoteStates=${remoteSummary || '-'}`,
|
||||
].join(';');
|
||||
}
|
||||
|
||||
function getCallDiagnosticsContext(call) {
|
||||
const pc = call?.pc || null;
|
||||
const nav = typeof navigator !== 'undefined' ? navigator : null;
|
||||
@@ -686,6 +743,7 @@ function getCallDiagnosticsContext(call) {
|
||||
const iceGatheringState = pc?.iceGatheringState || '';
|
||||
const currentLocalDescType = pc?.localDescription?.type || '';
|
||||
const currentRemoteDescType = pc?.remoteDescription?.type || '';
|
||||
const mediaSnapshot = buildMediaSnapshot(call);
|
||||
|
||||
return {
|
||||
remoteSessionIdPresent: Boolean(call?.remoteSessionId),
|
||||
@@ -707,6 +765,7 @@ function getCallDiagnosticsContext(call) {
|
||||
localAudioTracksCount: localAudioTracks.length,
|
||||
localAudioTracksEnabledCount: enabledLocalAudioTracks,
|
||||
localAudioTrackLabels: localAudioTracks.map((t) => String(t?.label || '')).join('|'),
|
||||
localTrackStates: summarizeMediaSnapshot(mediaSnapshot),
|
||||
hasPeerConnection: Boolean(pc),
|
||||
pcConnectionState: pc?.connectionState || '',
|
||||
pcIceConnectionState: pc?.iceConnectionState || '',
|
||||
@@ -808,6 +867,8 @@ async function flushPendingIceCandidates(call) {
|
||||
|
||||
async function closeMedia(call) {
|
||||
const pc = call?.pc || null;
|
||||
const beforeSnapshot = buildMediaSnapshot(call);
|
||||
recordCallTimeline(call, 'media_close_begin', summarizeMediaSnapshot(beforeSnapshot));
|
||||
try {
|
||||
const senders = pc?.getSenders?.() || [];
|
||||
senders.forEach((sender) => {
|
||||
@@ -822,6 +883,16 @@ async function closeMedia(call) {
|
||||
try { tr?.stop?.(); } catch {}
|
||||
});
|
||||
} catch {}
|
||||
try {
|
||||
if (pc) {
|
||||
pc.ontrack = null;
|
||||
pc.onicecandidate = null;
|
||||
pc.onconnectionstatechange = null;
|
||||
pc.oniceconnectionstatechange = null;
|
||||
pc.onsignalingstatechange = null;
|
||||
pc.onicegatheringstatechange = null;
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
call.localStream?.getTracks?.()?.forEach((track) => {
|
||||
try { track.enabled = false; } catch {}
|
||||
@@ -830,8 +901,15 @@ async function closeMedia(call) {
|
||||
} catch {}
|
||||
try {
|
||||
if (call.remoteAudio) {
|
||||
const remoteTracks = call.remoteAudio.srcObject?.getTracks?.() || [];
|
||||
remoteTracks.forEach((track) => {
|
||||
try { track.enabled = false; } catch {}
|
||||
try { track.stop(); } catch {}
|
||||
});
|
||||
try { call.remoteAudio.pause?.(); } catch {}
|
||||
try { call.remoteAudio.srcObject = null; } catch {}
|
||||
try { call.remoteAudio.removeAttribute?.('src'); } catch {}
|
||||
try { call.remoteAudio.load?.(); } catch {}
|
||||
call.remoteAudio = null;
|
||||
}
|
||||
} catch {}
|
||||
@@ -842,6 +920,14 @@ async function closeMedia(call) {
|
||||
call.connectionRouteLabel = '';
|
||||
call.connectionRouteDetails = '';
|
||||
call.pendingRemoteIceCandidates = [];
|
||||
const afterSnapshot = buildMediaSnapshot(call);
|
||||
recordCallTimeline(call, 'media_close_end', summarizeMediaSnapshot(afterSnapshot));
|
||||
await emitDebug(
|
||||
call,
|
||||
'info',
|
||||
'media_closed',
|
||||
`before=${summarizeMediaSnapshot(beforeSnapshot)} | after=${summarizeMediaSnapshot(afterSnapshot)}`,
|
||||
);
|
||||
}
|
||||
|
||||
function stopReconnectFlow(call) {
|
||||
|
||||
@@ -6,29 +6,116 @@ import {
|
||||
subscribeCallState,
|
||||
} from './call-service.js';
|
||||
|
||||
let shellEl = null;
|
||||
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 muteBtn = null;
|
||||
let acceptBtn = null;
|
||||
let declineBtn = null;
|
||||
let hangupBtn = null;
|
||||
let minimizeBtn = null;
|
||||
let barEl = null;
|
||||
let barTextEl = 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 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 (shellEl) return;
|
||||
if (rootEl) return;
|
||||
|
||||
shellEl = document.createElement('section');
|
||||
shellEl.className = 'call-overlay';
|
||||
shellEl.hidden = true;
|
||||
rootEl = document.createElement('section');
|
||||
rootEl.className = 'call-ui-root';
|
||||
rootEl.hidden = true;
|
||||
|
||||
barEl = document.createElement('button');
|
||||
barEl.type = 'button';
|
||||
barEl.className = 'call-minimized-bar';
|
||||
barEl.hidden = true;
|
||||
barEl.addEventListener('click', () => {
|
||||
if (!currentSnapshot) return;
|
||||
setMinimized(false);
|
||||
});
|
||||
|
||||
barTextEl = document.createElement('span');
|
||||
barTextEl.className = 'call-minimized-bar-text';
|
||||
barEl.append(barTextEl);
|
||||
|
||||
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 = 'secondary-btn call-overlay-minimize-btn';
|
||||
minimizeBtn.textContent = 'Свернуть';
|
||||
minimizeBtn.addEventListener('click', () => {
|
||||
if (!currentSnapshot) return;
|
||||
setMinimized(true);
|
||||
});
|
||||
|
||||
statusEl = document.createElement('div');
|
||||
statusEl.className = 'call-overlay-status';
|
||||
|
||||
@@ -69,20 +156,17 @@ function ensureUi() {
|
||||
});
|
||||
|
||||
controls.append(muteBtn, acceptBtn, declineBtn, hangupBtn);
|
||||
panelEl.append(titleEl, statusEl, controls);
|
||||
shellEl.append(panelEl);
|
||||
document.body.append(shellEl);
|
||||
headerEl.append(titleEl, minimizeBtn);
|
||||
panelEl.append(headerEl, statusEl, controls);
|
||||
overlayEl.append(panelEl);
|
||||
rootEl.append(barEl, overlayEl);
|
||||
resolveAppShell().append(rootEl);
|
||||
}
|
||||
|
||||
function applyCallState(snapshot) {
|
||||
ensureUi();
|
||||
function applyExpandedState(snapshot) {
|
||||
overlayEl.hidden = false;
|
||||
barEl.hidden = true;
|
||||
|
||||
if (!snapshot) {
|
||||
shellEl.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
shellEl.hidden = false;
|
||||
titleEl.textContent = `Звонок: ${snapshot.peerLogin || 'пользователь'}`;
|
||||
statusEl.textContent = snapshot.statusText || '';
|
||||
|
||||
@@ -95,10 +179,62 @@ function applyCallState(snapshot) {
|
||||
|
||||
acceptBtn.hidden = !snapshot.canAnswer;
|
||||
declineBtn.hidden = !snapshot.canDecline;
|
||||
|
||||
hangupBtn.hidden = !snapshot.canHangup;
|
||||
}
|
||||
|
||||
function applyMinimizedState(snapshot) {
|
||||
overlayEl.hidden = true;
|
||||
barEl.hidden = false;
|
||||
barTextEl.textContent = buildBarText(snapshot);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -1733,16 +1733,57 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.call-overlay {
|
||||
position: fixed;
|
||||
.call-ui-root[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.call-ui-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 80;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.call-minimized-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 44px;
|
||||
padding: calc(8px + env(safe-area-inset-top)) 14px 8px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(159, 255, 196, 0.28);
|
||||
background: linear-gradient(180deg, rgba(30, 138, 79, 0.98), rgba(18, 106, 60, 0.98));
|
||||
box-shadow: 0 10px 24px rgba(5, 30, 16, 0.28);
|
||||
color: #f3fff8;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
text-align: left;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.call-minimized-bar-text {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.call-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: rgba(6, 10, 16, 0.55);
|
||||
backdrop-filter: blur(4px);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.call-overlay-panel {
|
||||
@@ -1756,12 +1797,23 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.call-overlay-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.call-overlay-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
color: #eff4ff;
|
||||
}
|
||||
|
||||
.call-overlay-minimize-btn {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.call-overlay-status {
|
||||
color: #c8d7f6;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -24,6 +24,7 @@ body::before {
|
||||
width: min(100vw, 430px);
|
||||
height: 100dvh;
|
||||
position: relative;
|
||||
--call-minimized-bar-height: 0px;
|
||||
background: transparent;
|
||||
border-left: 1px solid rgba(211, 170, 86, 0.2);
|
||||
border-right: 1px solid rgba(211, 170, 86, 0.2);
|
||||
@@ -39,7 +40,7 @@ body::before {
|
||||
|
||||
.screen-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: var(--call-minimized-bar-height, 0px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 74px;
|
||||
@@ -52,6 +53,10 @@ body::before {
|
||||
padding-bottom: calc(24px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.app-shell.has-minimized-call .screen-content {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.screen-content.network-scroll-lock {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
Reference in New Issue
Block a user