self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); self.__shineStoppedCalls = self.__shineStoppedCalls || new Map(); self.addEventListener('message', (event) => { const data = event?.data || {}; if (data.type === 'SKIP_WAITING') { self.skipWaiting(); } }); async function broadcastToClients(payload) { const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); clients.forEach((client) => { client.postMessage({ type: 'SHINE_WEB_PUSH_EVENT', payload, }); }); } async function broadcastCallActionToClients(action, payload) { const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); clients.forEach((client) => { client.postMessage({ type: 'SHINE_CALL_PUSH_ACTION', action, payload, }); }); } function rememberStoppedCall(callId, sentAtMs = 0) { if (!callId) return; const now = Date.now(); const markAtMs = Number.isFinite(Number(sentAtMs)) ? Number(sentAtMs) : now; self.__shineStoppedCalls.set(callId, Math.max(now, markAtMs)); const cutoff = now - 10 * 60 * 1000; for (const [id, ts] of self.__shineStoppedCalls.entries()) { if (Number(ts || 0) < cutoff) self.__shineStoppedCalls.delete(id); } } function isCallStopped(callId, sentAtMs = 0) { if (!callId) return false; const stoppedAt = Number(self.__shineStoppedCalls.get(callId) || 0); if (!stoppedAt) return false; const incomingAt = Number.isFinite(Number(sentAtMs)) ? Number(sentAtMs) : 0; return incomingAt <= 0 || incomingAt <= stoppedAt; } async function closeCallNotification(callId) { if (!callId) return; const list = await self.registration.getNotifications({ tag: callId }); list.forEach((n) => { try { n.close(); } catch {} }); } function decodePushJson(rawText) { try { if (!rawText) return {}; return JSON.parse(rawText); } catch { return {}; } } function encodeCallPushPayloadForUrl(payload) { try { return encodeURIComponent(JSON.stringify(payload || {})); } catch { return ''; } } self.addEventListener('push', (event) => { let body = ''; let rawText = ''; let kind = ''; let fromLogin = ''; let title = ''; try { if (event.data) { const text = event.data.text(); rawText = text || ''; const json = decodePushJson(rawText); kind = String(json.kind || ''); title = String(json.title || ''); body = String(json.text || ''); fromLogin = String(json.fromLogin || ''); if (!kind && rawText) { body = rawText || ''; } } } catch { // ignore } const json = decodePushJson(rawText); const callId = String(json.callId || '').trim(); const fromSessionId = String(json.fromSessionId || '').trim(); const targetSessionId = String(json.targetSessionId || '').trim(); const toLogin = String(json.toLogin || '').trim(); const reason = String(json.reason || '').trim(); const sentAtMs = Number(json.sentAtMs || 0); const expiresAtMs = Number(json.expiresAtMs || 0); const nowMs = Date.now(); if (kind === 'stop_call' && callId) { rememberStoppedCall(callId, sentAtMs || nowMs); } const isExpiredIncomingCall = kind === 'incoming_call' && Number.isFinite(expiresAtMs) && expiresAtMs > 0 && nowMs > expiresAtMs; const isIncomingCallAlreadyStopped = kind === 'incoming_call' && callId && isCallStopped(callId, sentAtMs || nowMs); const shouldNotify = ( kind === 'new_message' || kind === 'test_push' || (kind === 'incoming_call' && !isExpiredIncomingCall && !isIncomingCallAlreadyStopped) || (!kind && body) ); const notificationTitle = kind === 'test_push' ? (title || 'SHiNE: тестовый push') : (kind === 'incoming_call' ? 'SHiNE: входящий звонок' : 'SHiNE: входящее сообщение'); const notifyPromise = shouldNotify ? self.registration.showNotification(notificationTitle, { body: body || (fromLogin ? `Вам пришло сообщение от ${fromLogin}` : 'Вам пришло сообщение'), tag: callId || (kind === 'test_push' ? 'shine-test-push' : 'shine-direct-message'), renotify: true, requireInteraction: kind === 'incoming_call', data: { kind, callId, fromLogin, fromSessionId, targetSessionId, toLogin, sentAtMs, expiresAtMs, reason, }, actions: kind === 'incoming_call' ? [ { action: 'accept', title: 'Ответить' }, { action: 'decline', title: 'Сбросить' }, ] : [], }) : Promise.resolve(); const closeOnStopPromise = kind === 'stop_call' && callId ? closeCallNotification(callId) : Promise.resolve(); event.waitUntil(Promise.all([ notifyPromise, closeOnStopPromise, broadcastToClients({ kind, body, fromLogin, fromSessionId, targetSessionId, toLogin, callId, sentAtMs, expiresAtMs, reason, stale: isExpiredIncomingCall || isIncomingCallAlreadyStopped, rawText, receivedAt: nowMs, }), ])); }); self.addEventListener('notificationclick', (event) => { event.notification?.close(); const action = String(event?.action || '').trim().toLowerCase(); const data = event?.notification?.data || {}; const payload = { kind: String(data.kind || '').trim(), callId: String(data.callId || '').trim(), fromLogin: String(data.fromLogin || '').trim(), fromSessionId: String(data.fromSessionId || '').trim(), targetSessionId: String(data.targetSessionId || '').trim(), toLogin: String(data.toLogin || '').trim(), sentAtMs: Number(data.sentAtMs || 0), expiresAtMs: Number(data.expiresAtMs || 0), reason: String(data.reason || '').trim(), }; event.waitUntil((async () => { if ((action === 'accept' || action === 'decline') && payload.callId) { await broadcastCallActionToClients(action, payload); } const allClients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); const existing = allClients.find((client) => { try { return client.url.includes('/index.html') || client.url.endsWith('/'); } catch { return false; } }); const openUrlBase = './index.html'; const encodedPayload = encodeCallPushPayloadForUrl(payload); const openUrl = (action === 'accept' || action === 'decline') ? `${openUrlBase}?callPushAction=${encodeURIComponent(action)}&callPushPayload=${encodedPayload}` : openUrlBase; if (existing) { try { if (action === 'accept' || action === 'decline') { existing.postMessage({ type: 'SHINE_CALL_PUSH_ACTION', action, payload, }); } } catch {} await existing.focus(); return; } await self.clients.openWindow(openUrl); })()); });