Push notifications: Use existing browser tab, unsubscribe on sign out (#1053)

This commit is contained in:
Alexander Zinchuk 2021-04-29 13:09:45 +03:00
parent b952967bdd
commit 9c4cb209c0
7 changed files with 136 additions and 90 deletions

View File

@ -1,34 +1,34 @@
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import React, {
FC, useState, useEffect, useCallback, useLayoutEffect, useRef, memo,
} from '../../lib/teact/teact';
import { withGlobal } from '../../lib/teact/teactn';
import { GlobalState, GlobalActions } from '../../global/types';
import {
MEDIA_CACHE_NAME,
MEDIA_CACHE_NAME_AVATARS,
MEDIA_PROGRESSIVE_CACHE_NAME,
CUSTOM_BG_CACHE_NAME,
LANG_CACHE_NAME,
} from '../../config';
import { IS_TOUCH_ENV } from '../../util/environment';
import * as cacheApi from '../../util/cacheApi';
import { formatPhoneNumber, getCountryFromPhoneNumber, getCountryById } from '../../util/phoneNumber';
import preloadFonts from '../../util/fonts';
import { preloadImage } from '../../util/files';
import { pick } from '../../util/iteratees';
import Button from '../ui/Button';
import InputText from '../ui/InputText';
import CountryCodeInput from './CountryCodeInput';
import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading';
// @ts-ignore // @ts-ignore
import monkeyPath from '../../assets/monkey.svg'; import monkeyPath from '../../assets/monkey.svg';
import {
CUSTOM_BG_CACHE_NAME,
LANG_CACHE_NAME,
MEDIA_CACHE_NAME,
MEDIA_CACHE_NAME_AVATARS,
MEDIA_PROGRESSIVE_CACHE_NAME,
} from '../../config';
import { GlobalActions, GlobalState } from '../../global/types';
import React, {
FC, memo, useCallback, useEffect, useLayoutEffect, useRef, useState,
} from '../../lib/teact/teact';
import { withGlobal } from '../../lib/teact/teactn';
import * as cacheApi from '../../util/cacheApi';
import { IS_TOUCH_ENV } from '../../util/environment';
import { preloadImage } from '../../util/files';
import preloadFonts from '../../util/fonts';
import { pick } from '../../util/iteratees';
import { formatPhoneNumber, getCountryById, getCountryFromPhoneNumber } from '../../util/phoneNumber';
import Button from '../ui/Button';
import Checkbox from '../ui/Checkbox';
import InputText from '../ui/InputText';
import Loading from '../ui/Loading';
import CountryCodeInput from './CountryCodeInput';
type StateProps = Pick<GlobalState, ( type StateProps = Pick<GlobalState, (
'connectionState' | 'authState' | 'connectionState' | 'authState' |
'authPhoneNumber' | 'authIsLoading' | 'authIsLoadingQrCode' | 'authError' | 'authRememberMe' | 'authNearestCountry' 'authPhoneNumber' | 'authIsLoading' | 'authIsLoadingQrCode' | 'authError' | 'authRememberMe' | 'authNearestCountry'

View File

@ -6,6 +6,7 @@ import { GlobalState } from '../../../global/types';
import { GRAMJS_SESSION_ID_KEY } from '../../../config'; import { GRAMJS_SESSION_ID_KEY } from '../../../config';
import { initApi, callApi } from '../../../api/gramjs'; import { initApi, callApi } from '../../../api/gramjs';
import { unsubscribeFromPush } from '../../../util/pushNotifications';
addReducer('initApi', (global: GlobalState, actions) => { addReducer('initApi', (global: GlobalState, actions) => {
const sessionId = localStorage.getItem(GRAMJS_SESSION_ID_KEY) || undefined; const sessionId = localStorage.getItem(GRAMJS_SESSION_ID_KEY) || undefined;
@ -101,6 +102,7 @@ addReducer('signOut', () => {
}); });
async function signOut() { async function signOut() {
await unsubscribeFromPush();
await callApi('destroy'); await callApi('destroy');
localStorage.removeItem(GRAMJS_SESSION_ID_KEY); localStorage.removeItem(GRAMJS_SESSION_ID_KEY);

View File

@ -12,7 +12,7 @@ import {
ApiUpdateCurrentUser, ApiUpdateCurrentUser,
} from '../../../api/types'; } from '../../../api/types';
import { DEBUG } from '../../../config'; import { DEBUG } from '../../../config';
import { setupPushNotifications } from '../../../util/setupPushNotifications'; import { subscribeToPush } from '../../../util/pushNotifications';
import { updateUser } from '../../reducers'; import { updateUser } from '../../reducers';
import { setLanguage } from '../../../util/langProvider'; import { setLanguage } from '../../../util/langProvider';
@ -57,6 +57,7 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
}); });
function onUpdateApiReady(global: GlobalState) { function onUpdateApiReady(global: GlobalState) {
subscribeToPush();
setLanguage(global.settings.byKey.language); setLanguage(global.settings.byKey.language);
} }
@ -139,7 +140,6 @@ function onUpdateConnectionState(update: ApiUpdateConnectionState) {
if (connectionState === 'connectionStateReady' && global.authState === 'authorizationStateReady') { if (connectionState === 'connectionStateReady' && global.authState === 'authorizationStateReady') {
getDispatch().sync(); getDispatch().sync();
setupPushNotifications();
} else if (connectionState === 'connectionStateBroken') { } else if (connectionState === 'connectionStateBroken') {
getDispatch().signOut(); getDispatch().signOut();
} }

View File

@ -1,6 +1,7 @@
import { DEBUG } from './config'; import { DEBUG } from './config';
import { respondForProgressive } from './serviceWorker/progressive'; import { respondForProgressive } from './serviceWorker/progressive';
import { respondWithCache, clearAssetCache } from './serviceWorker/assetCache'; import { respondWithCache, clearAssetCache } from './serviceWorker/assetCache';
import { handlePush, handleNotificationClick } from './serviceWorker/pushNotification';
declare const self: ServiceWorkerGlobalScope; declare const self: ServiceWorkerGlobalScope;
@ -45,54 +46,5 @@ self.addEventListener('fetch', (e: FetchEvent) => {
}); });
self.addEventListener('push', (e: PushEvent) => { self.addEventListener('push', handlePush);
if (DEBUG) { self.addEventListener('notificationclick', handleNotificationClick);
// 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);
}
}),
);
});

View File

@ -0,0 +1,80 @@
import { DEBUG } from '../config';
declare const self: ServiceWorkerGlobalScope;
export enum NotificationType {
MESSAGE_TEXT = 'MESSAGE_TEXT',
MESSAGE_NOTEXT = 'MESSAGE_NOTEXT',
MESSAGE_STICKER = 'MESSAGE_STICKER'
}
export type NotificationData = {
custom: {
msg_id: string;
from_id: string;
};
mute: '0' | '1';
badge: '0' | '1';
loc_key: NotificationType;
loc_args: string[];
random_id: number;
title: string;
description: string;
};
export function handlePush(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.json());
}
}
if (!e.data) return;
let data: NotificationData;
try {
data = e.data.json();
} catch (error) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('[SW] Unable to parse push notification data', e.data);
}
return;
}
const title = data.title || process.env.APP_INFO!;
const body = data.description;
const options = {
body,
icon: 'android-chrome-192x192.png',
};
e.waitUntil(
self.registration.showNotification(title, options),
);
}
export function handleNotificationClick(e: NotificationEvent) {
const appUrl = process.env.APP_URL!;
e.notification.close(); // Android needs explicit close.
e.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 === self.registration.scope && client.focus) {
client.focus();
return;
}
}
// If not, then open the target URL in a new window/tab.
if (self.clients.openWindow) {
self.clients.openWindow(appUrl);
}
}),
);
}

View File

@ -1,18 +1,20 @@
import { callApi } from '../api/gramjs'; import { callApi } from '../api/gramjs';
import { DEBUG } from '../config'; import { DEBUG } from '../config';
import { IS_SERVICE_WORKER_SUPPORTED } from './environment';
function getDeviceToken(subscription: PushSubscription) { function getDeviceToken(subscription: PushSubscription) {
const data = subscription.toJSON(); const data = subscription.toJSON();
return JSON.stringify({ endpoint: data.endpoint, keys: data.keys }); return JSON.stringify({ endpoint: data.endpoint, keys: data.keys });
} }
export async function setupPushNotifications() { export function isPushSupported() {
if (!IS_SERVICE_WORKER_SUPPORTED) return false;
if (!('showNotification' in ServiceWorkerRegistration.prototype)) { if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
if (DEBUG) { if (DEBUG) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('[PUSH] Push notifications aren\'t supported.'); console.log('[PUSH] Push notifications aren\'t supported.');
} }
return; return false;
} }
// Check the current Notification permission. // Check the current Notification permission.
@ -23,7 +25,7 @@ export async function setupPushNotifications() {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('[PUSH] The user has blocked push notifications.'); console.log('[PUSH] The user has blocked push notifications.');
} }
return; return false;
} }
// Check if push messaging is supported // Check if push messaging is supported
@ -32,11 +34,20 @@ export async function setupPushNotifications() {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('[PUSH] Push messaging isn\'t supported.'); console.log('[PUSH] Push messaging isn\'t supported.');
} }
return false;
} }
return true;
}
export async function unsubscribeFromPush() {
if (!isPushSupported) return;
const serviceWorkerRegistration = await navigator.serviceWorker.ready; const serviceWorkerRegistration = await navigator.serviceWorker.ready;
let subscription = await serviceWorkerRegistration.pushManager.getSubscription(); const subscription = await serviceWorkerRegistration.pushManager.getSubscription();
if (subscription) { if (subscription) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('[PUSH] Unsubscribing', subscription);
}
try { try {
const deviceToken = getDeviceToken(subscription); const deviceToken = getDeviceToken(subscription);
await callApi('unregisterDevice', deviceToken); await callApi('unregisterDevice', deviceToken);
@ -48,22 +59,22 @@ export async function setupPushNotifications() {
} }
} }
} }
}
export async function subscribeToPush() {
if (!isPushSupported()) return;
await unsubscribeFromPush();
const serviceWorkerRegistration = await navigator.serviceWorker.ready;
try { try {
subscription = await serviceWorkerRegistration.pushManager.subscribe({ const subscription = await serviceWorkerRegistration.pushManager.subscribe({
userVisibleOnly: true, userVisibleOnly: true,
}); });
const deviceToken = getDeviceToken(subscription); const deviceToken = getDeviceToken(subscription);
if (DEBUG) { if (DEBUG) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('[PUSH] Received push subscription: ', deviceToken); console.log('[PUSH] Received push subscription: ', deviceToken);
} }
const result = await callApi('registerDevice', deviceToken); await callApi('registerDevice', deviceToken);
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('[PUSH] registerDevice result', result);
}
} catch (error) { } catch (error) {
if (Notification.permission === 'denied' as NotificationPermission) { if (Notification.permission === 'denied' as NotificationPermission) {
// The user denied the notification permission which // The user denied the notification permission which

View File

@ -105,6 +105,7 @@ module.exports = (env = {}, argv = {}) => {
new EnvironmentPlugin({ new EnvironmentPlugin({
APP_INFO: 'Telegram T', APP_INFO: 'Telegram T',
APP_ENV: 'production', APP_ENV: 'production',
APP_URL: 'https://webz.telegram.org/',
TELEGRAM_T_API_ID: '', TELEGRAM_T_API_ID: '',
TELEGRAM_T_API_HASH: '', TELEGRAM_T_API_HASH: '',
}), }),