From dd3158001ee2ff62008ac060b2fe7df6d3186ed8 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 26 Apr 2021 15:47:10 +0300 Subject: [PATCH] Push Notifications: Initial support (#1046) --- public/site.webmanifest | 6 +- src/api/gramjs/methods/client.ts | 4 + src/api/gramjs/methods/index.ts | 2 +- src/api/gramjs/methods/settings.ts | 22 +++++- src/lib/gramjs/tl/apiTl.js | 2 + src/lib/gramjs/tl/static/api.reduced.tl | 2 + src/modules/actions/apiUpdaters/initial.ts | 2 + src/serviceWorker.ts | 53 ++++++++++++++ src/util/setupPushNotifications.ts | 85 ++++++++++++++++++++++ src/util/setupServiceWorker.ts | 2 +- 10 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 src/util/setupPushNotifications.ts diff --git a/public/site.webmanifest b/public/site.webmanifest index 87acd78a6..858168ebc 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -2,6 +2,7 @@ "name": "Telegram WebZ", "short_name": "Telegram WebZ", "start_url": "/", + "gcm_sender_id": "648693842861", "icons": [ { "src": "android-chrome-192x192.png", @@ -21,5 +22,8 @@ ], "theme_color": "#ffffff", "background_color": "#ffffff", - "display": "standalone" + "display": "standalone", + "permissions": [ + "notifications" + ] } diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index 39b4b4aad..adb2143f7 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -94,6 +94,10 @@ export async function destroy() { await client.destroy(); } +export function getClient() { + return client; +} + function handleGramJsUpdate(update: any) { if (update instanceof connection.UpdateConnectionState) { isConnected = update.state === connection.UpdateConnectionState.connected; diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index b343409d5..f8a80a3ba 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -44,7 +44,7 @@ export { updateProfilePhoto, uploadProfilePhoto, fetchWallpapers, uploadWallpaper, fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations, loadNotificationsSettings, updateContactSignUpNotification, updateNotificationSettings, - fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, + fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice, } from './settings'; export { diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index c6eb822a5..811b70b29 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -12,7 +12,7 @@ import { buildApiWallpaper, buildApiSession, buildPrivacyRules } from '../apiBui import { buildApiUser } from '../apiBuilders/users'; import { buildApiChatFromPreview, getApiChatIdFromMtpPeer } from '../apiBuilders/chats'; import { buildInputPrivacyKey, buildInputPeer, buildPeer } from '../gramjsBuilders'; -import { invokeRequest, uploadFile } from './client'; +import { invokeRequest, uploadFile, getClient } from './client'; import { omitVirtualClassFields } from '../apiBuilders/helpers'; import { buildCollectionByKey } from '../../../util/iteratees'; import localDb from '../localDb'; @@ -279,6 +279,26 @@ export async function fetchPrivacySettings(privacyKey: ApiPrivacyKey) { return buildPrivacyRules(result.rules); } +export function registerDevice(token: string) { + const client = getClient(); + const secret = client.session.getAuthKey().getKey(); + return invokeRequest(new GramJs.account.RegisterDevice({ + tokenType: 10, + secret, + appSandbox: false, + otherUids: [], + token, + })); +} + +export function unregisterDevice(token: string) { + return invokeRequest(new GramJs.account.UnregisterDevice({ + tokenType: 10, + otherUids: [], + token, + })); +} + export async function setPrivacySettings( privacyKey: ApiPrivacyKey, rules: IInputPrivacyRules, ) { diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 7180e3fb6..8bb177d2b 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -867,6 +867,8 @@ account.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings = account.confirmPasswordEmail#8fdf1920 code:string = Bool; account.getContactSignUpNotification#9f07c728 = Bool; account.setContactSignUpNotification#cff43f61 silent:Bool = Bool; +account.registerDevice#68976c6f flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector = Bool; +account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector = Bool; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#ca30a5b1 id:InputUser = UserFull; contacts.getContacts#c023849f hash:int = contacts.Contacts; diff --git a/src/lib/gramjs/tl/static/api.reduced.tl b/src/lib/gramjs/tl/static/api.reduced.tl index cf273263a..0e085042b 100644 --- a/src/lib/gramjs/tl/static/api.reduced.tl +++ b/src/lib/gramjs/tl/static/api.reduced.tl @@ -867,6 +867,8 @@ account.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings = account.confirmPasswordEmail#8fdf1920 code:string = Bool; account.getContactSignUpNotification#9f07c728 = Bool; account.setContactSignUpNotification#cff43f61 silent:Bool = Bool; +account.registerDevice#68976c6f flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector = Bool; +account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector = Bool; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#ca30a5b1 id:InputUser = UserFull; contacts.getContacts#c023849f hash:int = contacts.Contacts; diff --git a/src/modules/actions/apiUpdaters/initial.ts b/src/modules/actions/apiUpdaters/initial.ts index 6bbbd5296..e53e9960e 100644 --- a/src/modules/actions/apiUpdaters/initial.ts +++ b/src/modules/actions/apiUpdaters/initial.ts @@ -12,6 +12,7 @@ import { ApiUpdateCurrentUser, } from '../../../api/types'; import { DEBUG } from '../../../config'; +import { setupPushNotifications } from '../../../util/setupPushNotifications'; import { updateUser } from '../../reducers'; import { setLanguage } from '../../../util/langProvider'; @@ -138,6 +139,7 @@ function onUpdateConnectionState(update: ApiUpdateConnectionState) { if (connectionState === 'connectionStateReady' && global.authState === 'authorizationStateReady') { getDispatch().sync(); + setupPushNotifications(); } else if (connectionState === 'connectionStateBroken') { getDispatch().signOut(); } diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts index 346d6c775..9dcc1e989 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorker.ts @@ -43,3 +43,56 @@ self.addEventListener('fetch', (e: FetchEvent) => { return fetch(e.request); })()); }); + + +self.addEventListener('push', (e: PushEvent) => { + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('[SW] Push received event', e); + if (e.data) { + // eslint-disable-next-line no-console + console.log(`[SW] Push received with data "${e.data.text()}"`); + } + } + if (!e.data) return; + let obj; + try { + obj = e.data.json(); + } catch (error) { + obj = e.data.text(); + } + + const title = obj.title || 'Telegram'; + const body = obj.description || obj; + const options = { + body, + icon: 'android-chrome-192x192.png', + }; + + e.waitUntil( + self.registration.showNotification(title, options), + ); +}); + +self.addEventListener('notificationclick', (event) => { + const url = '/'; + event.notification.close(); // Android needs explicit close. + event.waitUntil( + self.clients.matchAll({ type: 'window' }) + .then((windowClients) => { + // Check if there is already a window/tab open with the target URL + for (let i = 0; i < windowClients.length; i++) { + const client = windowClients[i] as WindowClient; + // If so, just focus it. + if (client.url === url && client.focus) { + client.focus(); + return; + } + } + // If not, then open the target URL in a new window/tab. + if (self.clients.openWindow) { + self.clients.openWindow(url); + } + }), + ); +}); diff --git a/src/util/setupPushNotifications.ts b/src/util/setupPushNotifications.ts new file mode 100644 index 000000000..a352c8f27 --- /dev/null +++ b/src/util/setupPushNotifications.ts @@ -0,0 +1,85 @@ +import { callApi } from '../api/gramjs'; +import { DEBUG } from '../config'; + +function getDeviceToken(subscription: PushSubscription) { + const data = subscription.toJSON(); + return JSON.stringify({ endpoint: data.endpoint, keys: data.keys }); +} + +export async function setupPushNotifications() { + if (!('showNotification' in ServiceWorkerRegistration.prototype)) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('[PUSH] Push notifications aren\'t supported.'); + } + return; + } + + // Check the current Notification permission. + // If its denied, it's a permanent block until the + // user changes the permission + if (Notification.permission === 'denied') { + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('[PUSH] The user has blocked push notifications.'); + } + return; + } + + // Check if push messaging is supported + if (!('PushManager' in window)) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('[PUSH] Push messaging isn\'t supported.'); + } + } + + const serviceWorkerRegistration = await navigator.serviceWorker.ready; + let subscription = await serviceWorkerRegistration.pushManager.getSubscription(); + if (subscription) { + try { + const deviceToken = getDeviceToken(subscription); + await callApi('unregisterDevice', deviceToken); + await subscription.unsubscribe(); + } catch (error) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('[PUSH] Unable to unsubscribe from push.', error); + } + } + } + + try { + subscription = await serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true, + }); + + const deviceToken = getDeviceToken(subscription); + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('[PUSH] Received push subscription: ', deviceToken); + } + const result = await callApi('registerDevice', deviceToken); + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('[PUSH] registerDevice result', result); + } + } catch (error) { + if (Notification.permission === 'denied' as NotificationPermission) { + // The user denied the notification permission which + // means we failed to subscribe and the user will need + // to manually change the notification permission to + // subscribe to push messages + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('[PUSH] Permission for Notifications was denied'); + } + } else if (DEBUG) { + // A problem occurred with the subscription, this can + // often be down to an issue or lack of the gcm_sender_id + // and / or gcm_user_visible_only + // eslint-disable-next-line no-console + console.log('[PUSH] Unable to subscribe to push.', error); + } + } +} diff --git a/src/util/setupServiceWorker.ts b/src/util/setupServiceWorker.ts index d69e969a3..70956d643 100644 --- a/src/util/setupServiceWorker.ts +++ b/src/util/setupServiceWorker.ts @@ -1,8 +1,8 @@ import { scriptUrl } from 'service-worker-loader!../serviceWorker'; import { DEBUG } from '../config'; -import { IS_SERVICE_WORKER_SUPPORTED } from './environment'; import { getDispatch } from '../lib/teact/teactn'; +import { IS_SERVICE_WORKER_SUPPORTED } from './environment'; if (IS_SERVICE_WORKER_SUPPORTED) { window.addEventListener('load', async () => {