diff --git a/src/serviceWorker/pushNotification.ts b/src/serviceWorker/pushNotification.ts index fbade4411..d88e2a39c 100644 --- a/src/serviceWorker/pushNotification.ts +++ b/src/serviceWorker/pushNotification.ts @@ -32,9 +32,8 @@ type NotificationData = { }; let lastSyncAt = new Date().valueOf(); - -const clickBuffer: Record = {}; const shownNotifications = new Set(); +const clickBuffer: Record = {}; function getPushData(e: PushEvent | Notification): PushData | undefined { try { @@ -95,17 +94,32 @@ async function showNotification({ title, icon, }: NotificationData) { - await self.registration.showNotification(title, { + const tag = String(chatId || 0); + const options: NotificationOptions = { body, data: { chatId, messageId, + count: 1, }, icon: icon || 'icon-192x192.png', - badge: icon || 'icon-192x192.png', + badge: 'icon-192x192.png', + tag, vibrate: [200, 100, 200], - }); - await playNotificationSound(messageId || chatId || 0); + }; + const notifications = await self.registration.getNotifications({ tag }); + if (notifications.length > 0) { + const current = notifications[0]; + const count = current.data.count + 1; + options.data.count = count; + options.data.messageId = current.data.messageId; + options.body = `You have ${count} new messages`; + current.close(); + } + return Promise.all([ + playNotificationSound(messageId || chatId || 0), + self.registration.showNotification(title, options), + ]); } export function handlePush(e: PushEvent) { @@ -118,10 +132,6 @@ export function handlePush(e: PushEvent) { } } - // Do not show notifications right after sync (when browser is opened) - // To avoid stale notifications - if (new Date().valueOf() - lastSyncAt < 3000) return; - const data = getPushData(e); // Do not show muted notifications @@ -171,17 +181,25 @@ export function handleNotificationClick(e: NotificationEvent) { const clientsInScope = clients.filter((client) => { return new URL(client.url).origin === appUrl; }); - e.waitUntil(Promise.all(clientsInScope.map((client) => { + await Promise.all(clientsInScope.map((client) => { clickBuffer[client.id] = data; return focusChatMessage(client, data); - }))); + })); if (!self.clients.openWindow || clientsInScope.length > 0) return undefined; - + // Store notification data for default client (fix for android) + clickBuffer[0] = data; // If there is no opened client we need to open one and wait until it is fully loaded - const newClient = await self.clients.openWindow(appUrl); - if (newClient) { - // Store notification data until client is fully loaded - clickBuffer[newClient.id] = data; + try { + const newClient = await self.clients.openWindow(appUrl); + if (newClient) { + // Store notification data until client is fully loaded + clickBuffer[newClient.id] = data; + } + } catch (error) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.warn('[SW] ', error); + } } return undefined; }; @@ -197,21 +215,23 @@ export function handleClientMessage(e: ExtendableMessageEvent) { const source = e.source as WindowClient; if (e.data.type === 'clientReady') { // focus on chat message when client is fully ready - if (clickBuffer[source.id]) { - e.waitUntil(focusChatMessage(source, clickBuffer[source.id])); + const data = clickBuffer[source.id] || clickBuffer[0]; + if (data) { delete clickBuffer[source.id]; + delete clickBuffer[0]; + e.waitUntil(focusChatMessage(source, data)); } } if (e.data.type === 'newMessageNotification') { + // Do not show notifications right after sync (when browser is opened) + // To avoid stale notifications + if (new Date().valueOf() - lastSyncAt < 3000) return; + // store messageId for already shown notification - const notification: NotificationData = e.data.payload; - e.waitUntil(showNotification(notification)); - shownNotifications.add(notification.messageId); - } - if (e.data.type === 'notificationHandled') { const notification: NotificationData = e.data.payload; // mark this notification as shown if it was handled locally shownNotifications.add(notification.messageId); + e.waitUntil(showNotification(notification)); } } diff --git a/src/util/notifications.ts b/src/util/notifications.ts index bd1e36ada..fb987c423 100644 --- a/src/util/notifications.ts +++ b/src/util/notifications.ts @@ -3,7 +3,7 @@ import { ApiChat, ApiMediaFormat, ApiMessage, ApiUser, } from '../api/types'; import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText'; -import { APP_NAME, DEBUG } from '../config'; +import { DEBUG } from '../config'; import { getDispatch, getGlobal, setGlobal } from '../lib/teact/teactn'; import { getChatAvatarHash, @@ -313,123 +313,6 @@ async function getAvatar(chat: ApiChat) { return mediaData; } -type NotificationData = { - messageId?: number; - chatId?: number; - title: string; - body: string; - icon?: string; -}; - -const handledNotifications = new Set(); -let pendingNotifications: Record = {}; - -async function showNotifications(groupLimit: number = 2) { - const count = Object.keys(pendingNotifications).reduce((result, groupId) => { - result += pendingNotifications[Number(groupId)].length; - return result; - }, 0); - // if we have more than groupLimit notification groups we send only one notification - if (Object.keys(pendingNotifications).length > groupLimit) { - await showNotification({ - title: APP_NAME, - body: `You have ${count} new Telegram notifications`, - }); - } else { - // Else we send a notification per group - await Promise.all(Object.keys(pendingNotifications) - // eslint-disable-next-line no-async-without-await/no-async-without-await - .map(async (groupId) => { - const group = pendingNotifications[Number(groupId)]; - if (group.length > groupLimit) { - const lastMessage = group[group.length - 1]; - return showNotification({ - title: APP_NAME, - body: `You have ${count} notifications from ${lastMessage.title}`, - messageId: lastMessage.messageId, - chatId: Number(groupId), - }); - } - return Promise.all(group.map(showNotification)); - })); - } - - // Clear all pending notifications - pendingNotifications = {}; -} - -const flushNotifications = debounce(showNotifications, 1000, false); - -async function handleNotification(data: NotificationData, groupLimit?: number) { - // Dont show already triggered notification - if (handledNotifications.has(data.messageId)) { - handledNotifications.delete(data.messageId); - return; - } - - const groupId = data.chatId || 0; - if (!pendingNotifications[groupId]) { - pendingNotifications[groupId] = []; - } - pendingNotifications[groupId].push(data); - await flushNotifications(groupLimit); - - if (checkIfPushSupported()) { - if (navigator.serviceWorker.controller) { - // notify service worker that notification was handled locally - navigator.serviceWorker.controller.postMessage({ - type: 'notificationHandled', - payload: data, - }); - } - } - - handledNotifications.add(data.messageId); -} - -function showNotification(data: NotificationData) { - if (checkIfPushSupported()) { - if (navigator.serviceWorker.controller) { - // notify service worker about new message notification - navigator.serviceWorker.controller.postMessage({ - type: 'newMessageNotification', - payload: data, - }); - } - } else { - const dispatch = getDispatch(); - const options: NotificationOptions = { - body: data.body, - icon: data.icon, - badge: data.icon, - tag: data.messageId ? data.messageId.toString() : undefined, - }; - - if ('vibrate' in navigator) { - options.vibrate = [200, 100, 200]; - } - - const notification = new Notification(data.title, options); - - notification.onclick = () => { - notification.close(); - dispatch.focusMessage({ - chatId: data.chatId, - messageId: data.messageId, - }); - if (window.focus) { - window.focus(); - } - }; - - // Play sound when notification is displayed - notification.onshow = () => { - const id = data.messageId || data.chatId; - if (id) playNotificationSound(id); - }; - } -} - export async function showNewMessageNotification({ chat, message, @@ -448,13 +331,51 @@ export async function showNewMessageNotification({ const icon = await getAvatar(chat); - await handleNotification({ - title, - body, - icon, - messageId: message.id, - chatId: chat.id, - }); + if (checkIfPushSupported()) { + if (navigator.serviceWorker.controller) { + // notify service worker about new message notification + navigator.serviceWorker.controller.postMessage({ + type: 'newMessageNotification', + payload: { + title, + body, + icon, + chatId: chat.id, + messageId: message.id, + }, + }); + } + } else { + const dispatch = getDispatch(); + const options: NotificationOptions = { + body, + icon, + badge: icon, + tag: message.id.toString(), + }; + + if ('vibrate' in navigator) { + options.vibrate = [200, 100, 200]; + } + + const notification = new Notification(title, options); + + notification.onclick = () => { + notification.close(); + dispatch.focusMessage({ + chatId: chat.id, + messageId: message.id, + }); + if (window.focus) { + window.focus(); + } + }; + + // Play sound when notification is displayed + notification.onshow = () => { + playNotificationSound(message.id || chat.id); + }; + } } // Notify service worker that client is fully loaded diff --git a/src/util/setupServiceWorker.ts b/src/util/setupServiceWorker.ts index aaa9bf1ed..7ed4da3cf 100644 --- a/src/util/setupServiceWorker.ts +++ b/src/util/setupServiceWorker.ts @@ -10,6 +10,10 @@ type WorkerAction = { function handleWorkerMessage(e: MessageEvent) { const action: WorkerAction = e.data; + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('[SW] Message from worker', action); + } if (!action.type) return; const dispatch = getDispatch(); switch (action.type) {