232 lines
6.8 KiB
JavaScript
232 lines
6.8 KiB
JavaScript
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 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,
|
|
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,
|
|
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(),
|
|
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);
|
|
})());
|
|
});
|