/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE * * Originally from: * https://github.com/zhukov/webogram * Copyright (C) 2014 Igor Zhukov * https://github.com/zhukov/webogram/blob/master/LICENSE */ import type { NotificationSettings } from "../appManagers/appNotificationsManager"; import { MOUNT_CLASS_TO } from "../../config/debug"; import { copy } from "../../helpers/object"; import { logger } from "../logger"; import rootScope from "../rootScope"; import { ServiceWorkerNotificationsClearTask, ServiceWorkerPingTask, ServiceWorkerPushClickTask } from "../serviceWorker/index.service"; import apiManager from "./mtprotoworker"; import I18n, { LangPackKey } from "../langPack"; import { isMobile } from "../../helpers/userAgent"; export type PushSubscriptionNotifyType = 'init' | 'subscribe' | 'unsubscribe'; export type PushSubscriptionNotifyEvent = `push_${PushSubscriptionNotifyType}`; export type PushSubscriptionNotify = { tokenType: number, tokenValue: string }; export class WebPushApiManager { public isAvailable = true; private isPushEnabled = false; private localNotificationsAvailable = true; private started = false; private settings: NotificationSettings & {baseUrl?: string} = {} as any; private isAliveTO: any; private isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; private userVisibleOnly = this.isFirefox ? false : true; private log = logger('PM'); constructor() { if(!('PushManager' in window) || !('Notification' in window) || !('serviceWorker' in navigator)) { this.log.warn('Push messaging is not supported.'); this.isAvailable = false; this.localNotificationsAvailable = false; } if(this.isAvailable && Notification.permission === 'denied') { this.log.warn('The user has blocked notifications.'); } } public start() { if(!this.started) { this.started = true; this.getSubscription(); this.setUpServiceWorkerChannel(); } } public setLocalNotificationsDisabled() { this.localNotificationsAvailable = false; } public getSubscription() { if(!this.isAvailable) { return; } navigator.serviceWorker.ready.then((reg) => { reg.pushManager.getSubscription().then((subscription) => { this.isPushEnabled = !!subscription; this.pushSubscriptionNotify('init', subscription); }).catch((err) => { this.log.error('Error during getSubscription()', err); }); }); } public subscribe = () => { if(!this.isAvailable) { return; } navigator.serviceWorker.ready.then((reg) => { reg.pushManager.subscribe({userVisibleOnly: this.userVisibleOnly}).then((subscription) => { // The subscription was successful this.isPushEnabled = true; this.pushSubscriptionNotify('subscribe', subscription); }).catch((e) => { if(Notification.permission === 'denied') { this.log('Permission for Notifications was denied'); } else { this.log('Unable to subscribe to push.', e); if(!this.userVisibleOnly) { this.userVisibleOnly = true; setTimeout(this.subscribe, 0); } } }); }); } public unsubscribe() { if(!this.isAvailable) { return; } navigator.serviceWorker.ready.then((reg) => { reg.pushManager.getSubscription().then((subscription) => { this.isPushEnabled = false; if(subscription) { this.pushSubscriptionNotify('unsubscribe', subscription); setTimeout(() => { subscription.unsubscribe().then((successful) => { this.isPushEnabled = false; }).catch((e) => { this.log.error('Unsubscription error: ', e); }); }, 3000); } }).catch((e) => { this.log.error('Error thrown while unsubscribing from ' + 'push messaging.', e); }); }); } public forceUnsubscribe() { if(!this.isAvailable) { return; } navigator.serviceWorker.ready.then((reg) => { reg.pushManager.getSubscription().then((subscription) => { this.log.warn('force unsubscribe', subscription); if(subscription) { subscription.unsubscribe().then((successful) => { this.log.warn('force unsubscribe successful', successful); this.isPushEnabled = false; }).catch((e) => { this.log.error('Unsubscription error: ', e); }); } }).catch((e) => { this.log.error('Error thrown while unsubscribing from ' + 'push messaging.', e); }); }); } public isAliveNotify = () => { if(!this.isAvailable || rootScope.idle && rootScope.idle.deactivated) { return; } this.settings.baseUrl = (location.href || '').replace(/#.*$/, '') + '#/im'; const lang: ServiceWorkerPingTask['payload']['lang'] = {} as any; const ACTIONS_LANG_MAP: Record = { push_action_mute1d: isMobile ? 'PushNotification.Action.Mute1d.Mobile' : 'PushNotification.Action.Mute1d', push_action_settings: isMobile ? 'PushNotification.Action.Settings.Mobile' : 'PushNotification.Action.Settings', push_message_nopreview: 'PushNotification.Message.NoPreview' }; for(const action in ACTIONS_LANG_MAP) { lang[action as keyof typeof ACTIONS_LANG_MAP] = I18n.format(ACTIONS_LANG_MAP[action as keyof typeof ACTIONS_LANG_MAP], true); } const task: ServiceWorkerPingTask = { type: 'ping', payload: { localNotifications: this.localNotificationsAvailable, lang: lang, settings: this.settings } }; if(navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage(task); } this.isAliveTO = setTimeout(this.isAliveNotify, 10000); } public setSettings(newSettings: WebPushApiManager['settings']) { this.settings = copy(newSettings); clearTimeout(this.isAliveTO); this.isAliveNotify(); } public hidePushNotifications() { if(!this.isAvailable) { return; } if(navigator.serviceWorker.controller) { const task: ServiceWorkerNotificationsClearTask = {type: 'notifications_clear'}; navigator.serviceWorker.controller.postMessage(task); } } public setUpServiceWorkerChannel() { if(!this.isAvailable) { return; } apiManager.addServiceWorkerTaskListener('push_click', (task: ServiceWorkerPushClickTask) => { if(rootScope.idle && rootScope.idle.deactivated) { // AppRuntimeManager.reload(); // WARNING location.reload(); return; } rootScope.dispatchEvent('push_notification_click', task.payload); }); navigator.serviceWorker.ready.then(this.isAliveNotify); } public pushSubscriptionNotify(event: PushSubscriptionNotifyType, subscription?: PushSubscription) { if(subscription) { const subscriptionObj: PushSubscriptionJSON = subscription.toJSON(); if(!subscriptionObj || !subscriptionObj.endpoint || !subscriptionObj.keys || !subscriptionObj.keys.p256dh || !subscriptionObj.keys.auth) { this.log.warn('Invalid push subscription', subscriptionObj); this.unsubscribe(); this.isAvailable = false; this.pushSubscriptionNotify(event); return; } this.log.warn('Push', event, subscriptionObj); rootScope.dispatchEvent(('push_' + event) as PushSubscriptionNotifyEvent, { tokenType: 10, tokenValue: JSON.stringify(subscriptionObj) }); } else { this.log.warn('Push', event, false); rootScope.dispatchEvent(('push_' + event) as PushSubscriptionNotifyEvent, false as any); } } } const webPushApiManager = new WebPushApiManager(); MOUNT_CLASS_TO && (MOUNT_CLASS_TO.webPushApiManager = webPushApiManager); export default webPushApiManager;