From 5e8103a6daaccd4492974bd8b480c054a0449e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=D1=81=20=D0=A1=D1=83=D0=BB=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Wed, 26 Aug 2026 14:54:37 +0300 Subject: [PATCH] =?UTF-8?q?fix(push):=20bump=20SW=20to=20v12,=20=D1=83?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=20openOrFocusChat=20=D0=B8?= =?UTF-8?q?=20notificationclick?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - openOrFocusChat: includeUncontrolled=true, логирование, try/catch, fallback на clients.openWindow. - notificationclick: логирование data, fallback построения URL из вложенного data.data. - markNotificationRead: поддержка type/messageId/conversationId на верхнем уровне и во вложенном data. - Bump CACHE_NAME до v12 для принудительного обновления SW. --- client/public/sw.js | 100 +++++++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/client/public/sw.js b/client/public/sw.js index d867b3d..735dffb 100644 --- a/client/public/sw.js +++ b/client/public/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'iistwin-pwa-v11'; +const CACHE_NAME = 'iistwin-pwa-v12'; const STATIC_ASSETS = [ '/', '/index.html', @@ -568,54 +568,73 @@ self.addEventListener('push', (event) => { }); async function openOrFocusChat(url) { - // Only controlled clients can be navigated/focused reliably from a SW. - const clientList = await clients.matchAll({ type: 'window', includeUncontrolled: false }); const absoluteUrl = new URL(url, self.location.origin).href; - const targetPath = new URL(absoluteUrl).pathname; + console.log('[SW] openOrFocusChat:', absoluteUrl); - // Prefer a controlled client that already has the same path (ignoring query string). - for (const client of clientList) { - try { - const clientPath = new URL(client.url).pathname; - if (clientPath === targetPath && 'focus' in client) { - await client.navigate(absoluteUrl); - return await client.focus(); + try { + // Ищем любые окна (включая неподконтрольные) — focus работает и на них, + // а navigate может не сработать на uncontrolled, поэтому оборачиваем в try/catch. + const clientList = await clients.matchAll({ type: 'window', includeUncontrolled: true }); + console.log('[SW] clients found:', clientList.length); + + // Сначала ищем клиент с уже открытым этим URL — просто фокусируем. + for (const client of clientList) { + try { + if (client.url === absoluteUrl && 'focus' in client) { + console.log('[SW] focusing existing client with same URL'); + return await client.focus(); + } + } catch (err) { + console.error('[SW] focus same-url client error:', err); } - } catch (err) { - console.error('[SW] Failed to navigate matching client:', err); } - } - // Otherwise focus any controlled same-origin client and navigate it. - for (const client of clientList) { - try { - if ('focus' in client) { - await client.navigate(absoluteUrl); - return await client.focus(); + // Затем пытаемся навигировать любой доступный клиент. + for (const client of clientList) { + try { + if ('focus' in client) { + console.log('[SW] navigating client to', absoluteUrl); + await client.navigate(absoluteUrl); + return await client.focus(); + } + } catch (err) { + console.error('[SW] navigate client error:', err); } - } catch (err) { - console.error('[SW] Failed to navigate client:', err); } - } - // No usable client — open a new window/tab. This is always allowed in notificationclick. - return await clients.openWindow(absoluteUrl); + // Нет подходящих клиентов — открываем новое окно/вкладку. + console.log('[SW] opening new window'); + const newClient = await clients.openWindow(absoluteUrl); + if (newClient && 'focus' in newClient) { + return await newClient.focus(); + } + return newClient; + } catch (err) { + console.error('[SW] openOrFocusChat fatal error:', err); + // Последний fallback — всё равно пытаемся открыть окно. + return clients.openWindow(absoluteUrl); + } } async function markNotificationRead(data) { try { - if (data?.type === 'chat' && data?.messageId) { - await fetch(`/api/messages/${data.messageId}/read`, { + // Поддерживаем оба формата: type на верхнем уровне и вложенный data.type + const type = data?.type || data?.data?.type; + const messageId = data?.messageId || data?.data?.messageId; + const conversationId = data?.conversationId || data?.data?.conversationId; + + if (type === 'chat' && messageId) { + await fetch(`/api/messages/${messageId}/read`, { method: 'POST', credentials: 'include', }); - console.log('[SW] Marked chat message as read:', data.messageId); - } else if (data?.type === 'messenger' && data?.conversationId) { - await fetch(`/api/messenger/conversations/${data.conversationId}/read`, { + console.log('[SW] Marked chat message as read:', messageId); + } else if (type === 'messenger' && conversationId) { + await fetch(`/api/messenger/conversations/${conversationId}/read`, { method: 'POST', credentials: 'include', }); - console.log('[SW] Marked messenger conversation as read:', data.conversationId); + console.log('[SW] Marked messenger conversation as read:', conversationId); } } catch (err) { console.error('[SW] Failed to mark notification as read:', err); @@ -623,18 +642,31 @@ async function markNotificationRead(data) { } self.addEventListener('notificationclick', (event) => { - console.log('[SW] Notification clicked', event.action); + console.log('[SW] Notification clicked, action:', event.action); event.notification.close(); const data = event.notification.data || {}; - const urlToOpen = data.url || '/'; + console.log('[SW] Notification data:', JSON.stringify(data)); + + // Fallback: если url отсутствует, пытаемся восстановить его из вложенного data. + let urlToOpen = data.url || '/'; + if ((!data.url || data.url === '/') && data.data) { + const nested = data.data; + if (nested.type === 'chat' && nested.taskId && nested.formId) { + urlToOpen = `/forms/${nested.formId}/tasks/${nested.taskId}${nested.isChat === 'true' ? '?tab=chat' : ''}`; + console.log('[SW] Rebuilt url from nested data:', urlToOpen); + } else if (nested.type === 'messenger' && nested.conversationId) { + urlToOpen = `/chat/${nested.conversationId}`; + console.log('[SW] Rebuilt url from nested data:', urlToOpen); + } + } if (event.action === 'read') { // "Прочитано": помечаем прочитанным и всё равно открываем задачу/чат, как ожидает пользователь. event.waitUntil( Promise.all([ navigator.clearAppBadge ? navigator.clearAppBadge() : Promise.resolve(), - markNotificationRead(data), + markNotificationRead(data.data || data), openOrFocusChat(urlToOpen), ]) );