Push Notifications: Initial support (#1046)

This commit is contained in:
Alexander Zinchuk 2021-04-26 15:47:10 +03:00
parent e15ca141ae
commit dd3158001e
10 changed files with 176 additions and 4 deletions

View File

@ -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"
]
}

View File

@ -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;

View File

@ -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 {

View File

@ -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,
) {

View File

@ -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<int> = Bool;
account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector<int> = Bool;
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#ca30a5b1 id:InputUser = UserFull;
contacts.getContacts#c023849f hash:int = contacts.Contacts;

View File

@ -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<int> = Bool;
account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector<int> = Bool;
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#ca30a5b1 id:InputUser = UserFull;
contacts.getContacts#c023849f hash:int = contacts.Contacts;

View File

@ -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();
}

View File

@ -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);
}
}),
);
});

View File

@ -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);
}
}
}

View File

@ -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 () => {