diff --git a/src/serviceWorker/pushNotification.ts b/src/serviceWorker/pushNotification.ts index 9e545b269..c6a59dfc6 100644 --- a/src/serviceWorker/pushNotification.ts +++ b/src/serviceWorker/pushNotification.ts @@ -74,11 +74,17 @@ function getNotificationData(data: PushData): NotificationData { } function showNotification({ - chatId, messageId, body, title, + chatId, + messageId, + body, + title, }: NotificationData) { return self.registration.showNotification(title, { body, - data: { chatId, messageId }, + data: { + chatId, + messageId, + }, icon: 'icon-192x192.png', badge: 'icon-192x192.png', vibrate: [200, 100, 200], @@ -110,8 +116,11 @@ export function handlePush(e: PushEvent) { e.waitUntil(showNotification(notification)); } -function focusChatMessage(client: WindowClient, data: { chatId?: number; messageId?: number }) { - const { chatId, messageId } = data; +async function focusChatMessage(client: WindowClient, data: { chatId?: number; messageId?: number }) { + const { + chatId, + messageId, + } = data; if (!chatId) return; client.postMessage({ type: 'focusMessage', @@ -120,6 +129,15 @@ function focusChatMessage(client: WindowClient, data: { chatId?: number; message messageId, }, }); + // Catch "focus not allowed" DOM Exceptions + try { + await client.focus(); + } catch (error) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.warn('[SW] ', error); + } + } } export function handleNotificationClick(e: NotificationEvent) { @@ -129,10 +147,10 @@ export function handleNotificationClick(e: NotificationEvent) { const notifyClients = async () => { const clients = await self.clients.matchAll({ type: 'window' }) as WindowClient[]; const clientsInScope = clients.filter((client) => client.url === self.registration.scope); - await Promise.all(clientsInScope.map(async (client) => { - await client.focus(); + e.waitUntil(Promise.all(clientsInScope.map((client) => { clickBuffer[client.id] = data; - })); + return focusChatMessage(client, data); + }))); if (!self.clients.openWindow || clientsInScope.length > 0) return undefined; // If there is no opened client we need to open one and wait until it is fully loaded @@ -147,12 +165,16 @@ export function handleNotificationClick(e: NotificationEvent) { } export function handleClientMessage(e: ExtendableMessageEvent) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('[SW] New message from client', e); + } if (!e.data) return; const source = e.source as WindowClient; if (e.data.type === 'clientReady') { // focus on chat message when client is fully ready if (clickBuffer[source.id]) { - focusChatMessage(source, clickBuffer[source.id]); + e.waitUntil(focusChatMessage(source, clickBuffer[source.id])); delete clickBuffer[source.id]; } } diff --git a/src/util/notifications.ts b/src/util/notifications.ts index 13b963ff8..874586fb1 100644 --- a/src/util/notifications.ts +++ b/src/util/notifications.ts @@ -2,7 +2,7 @@ import { callApi } from '../api/gramjs'; import { ApiChat, ApiMessage } from '../api/types'; import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText'; import { DEBUG } from '../config'; -import { getDispatch, getGlobal } from '../lib/teact/teactn'; +import { getDispatch, getGlobal, setGlobal } from '../lib/teact/teactn'; import { getChatTitle, getMessageAction, @@ -12,6 +12,7 @@ import { isActionMessage, isChatChannel, } from '../modules/helpers'; +import { replaceSettings } from '../modules/reducers'; import { selectChatMessage, selectUser } from '../modules/selectors'; import { IS_SERVICE_WORKER_SUPPORTED } from './environment'; @@ -122,9 +123,22 @@ export async function unsubscribe() { await unsubscribeFromPush(subscription); } +// Load notification settings from the api +async function loadNotificationsSettings() { + const result = await callApi('loadNotificationsSettings'); + if (!result) return; + setGlobal(replaceSettings(getGlobal(), result)); +} + export async function subscribe() { - await requestPermission(); - if (!checkIfPushSupported()) return; + loadNotificationsSettings(); + + if (!checkIfPushSupported()) { + // Ask for notification permissions only if service worker notifications are not supported + // As pushManager.subscribe automatically triggers permission popup + await requestPermission(); + return; + } const serviceWorkerRegistration = await navigator.serviceWorker.ready; let subscription = await serviceWorkerRegistration.pushManager.getSubscription(); if (!checkIfShouldResubscribe(subscription)) return; @@ -157,17 +171,22 @@ export async function subscribe() { // and / or gcm_user_visible_only // eslint-disable-next-line no-console console.log('[PUSH] Unable to subscribe to push.', error); + + // Request permissions and fall back to local notifications + // if pushManager.subscribe was aborted due to invalid VAPID key. + if (error.code === DOMException.ABORT_ERR) { + await requestPermission(); + } } } } -async function checkIfShouldNotify(chat: ApiChat, isActive: boolean) { - if (chat.isMuted) return false; +function checkIfShouldNotify(chat: ApiChat, isActive: boolean) { + if (chat.isMuted || chat.isNotJoined) return false; // Dont show notification for active chat if client has focus if (isActive && document.hasFocus()) return false; - await getDispatch().loadNotificationsSettings(); const global = getGlobal(); switch (chat.type) { case 'chatTypePrivate': @@ -226,7 +245,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) { }; } -export async function showNewMessageNotification({ +export function showNewMessageNotification({ chat, message, isActiveChat, @@ -234,8 +253,7 @@ export async function showNewMessageNotification({ if (!checkIfNotificationsSupported()) return; if (!message.id) return; - const shouldNotify = await checkIfShouldNotify(chat, isActiveChat); - if (!shouldNotify) return; + if (!checkIfShouldNotify(chat, isActiveChat)) return; const { title,