Push notifications: Use existing browser tab, unsubscribe on sign out (#1053)
This commit is contained in:
parent
b952967bdd
commit
9c4cb209c0
@ -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'
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|||||||
80
src/serviceWorker/pushNotification.ts
Normal file
80
src/serviceWorker/pushNotification.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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: '',
|
||||||
}),
|
}),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user