SHiNE-server/shine-UI/firebase-messaging-sw.js

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);
})());
});