mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-22 05:11:55 +01:00
Push Notifications: Open chat message on notification click, various fixes (#1055)
This commit is contained in:
parent
5fb8c40cec
commit
e053013219
@ -110,6 +110,7 @@ function updateCache() {
|
||||
'chatFolders',
|
||||
'topPeers',
|
||||
'recentEmojis',
|
||||
'push',
|
||||
]),
|
||||
isChatInfoShown: reduceShowChatInfo(global),
|
||||
users: reduceUsers(global),
|
||||
|
@ -375,6 +375,11 @@ export type GlobalState = {
|
||||
error?: string;
|
||||
waitingEmailCodeLength?: number;
|
||||
};
|
||||
|
||||
push?: {
|
||||
deviceToken: string;
|
||||
subscribedAt: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type ActionTypes = (
|
||||
@ -439,7 +444,8 @@ export type ActionTypes = (
|
||||
'clickInlineButton' | 'sendBotCommand' |
|
||||
// misc
|
||||
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'closeAudioPlayer' | 'openPollModal' | 'closePollModal' |
|
||||
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' |
|
||||
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' |
|
||||
'deleteDeviceToken' |
|
||||
// payment
|
||||
'openPaymentModal' | 'closePaymentModal' |
|
||||
'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' |
|
||||
|
@ -140,3 +140,20 @@ addReducer('loadNearestCountry', (global) => {
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
|
||||
addReducer('setDeviceToken', (global, actions, deviceToken) => {
|
||||
setGlobal({
|
||||
...global,
|
||||
push: {
|
||||
deviceToken,
|
||||
subscribedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
addReducer('deleteDeviceToken', (global) => {
|
||||
const newGlobal = { ...global };
|
||||
delete newGlobal.push;
|
||||
setGlobal(newGlobal);
|
||||
});
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
} from '../../../config';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { notifyClientReady } from '../../../util/pushNotifications';
|
||||
import {
|
||||
replaceChatListIds,
|
||||
replaceChats,
|
||||
@ -41,6 +42,9 @@ async function sync(afterSyncCallback: () => void) {
|
||||
console.log('>>> START SYNC');
|
||||
}
|
||||
|
||||
// Notify web worker that client is ready to receive messages
|
||||
notifyClientReady();
|
||||
|
||||
await callApi('fetchCurrentUser');
|
||||
|
||||
// This fetches only active chats and clears archived chats, which will be fetched in `afterSync`
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { DEBUG } from './config';
|
||||
import { respondForProgressive } from './serviceWorker/progressive';
|
||||
import { respondWithCache, clearAssetCache } from './serviceWorker/assetCache';
|
||||
import { handlePush, handleNotificationClick } from './serviceWorker/pushNotification';
|
||||
import { handlePush, handleNotificationClick, handleClientMessage } from './serviceWorker/pushNotification';
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope;
|
||||
|
||||
@ -48,3 +48,4 @@ self.addEventListener('fetch', (e: FetchEvent) => {
|
||||
|
||||
self.addEventListener('push', handlePush);
|
||||
self.addEventListener('notificationclick', handleNotificationClick);
|
||||
self.addEventListener('message', handleClientMessage);
|
||||
|
@ -2,26 +2,35 @@ 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;
|
||||
msg_id?: string;
|
||||
channel_id?: string;
|
||||
chat_id?: string;
|
||||
from_id?: string;
|
||||
};
|
||||
mute: '0' | '1';
|
||||
badge: '0' | '1';
|
||||
loc_key: NotificationType;
|
||||
loc_key: string;
|
||||
loc_args: string[];
|
||||
random_id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const clickBuffer: Record<string, NotificationData> = {};
|
||||
|
||||
function getPushData(e: PushEvent | Notification): NotificationData | undefined {
|
||||
try {
|
||||
return 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 undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function handlePush(e: PushEvent) {
|
||||
if (DEBUG) {
|
||||
@ -32,49 +41,92 @@ export function handlePush(e: PushEvent) {
|
||||
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 data = getPushData(e);
|
||||
if (!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),
|
||||
self.registration.showNotification(title, {
|
||||
body,
|
||||
data,
|
||||
icon: 'android-chrome-192x192.png',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function getChatId(data: NotificationData) {
|
||||
if (data.custom.from_id) {
|
||||
return parseInt(data.custom.from_id, 10);
|
||||
}
|
||||
// Chats and channels have negative IDs
|
||||
if (data.custom.chat_id) {
|
||||
return parseInt(data.custom.chat_id, 10) * -1;
|
||||
}
|
||||
if (data.custom.channel_id) {
|
||||
return parseInt(data.custom.channel_id, 10) * -1;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getMessageId(data: NotificationData) {
|
||||
if (!data.custom.msg_id) return undefined;
|
||||
return parseInt(data.custom.msg_id, 10);
|
||||
}
|
||||
|
||||
function focusChatMessage(client: WindowClient, data: NotificationData) {
|
||||
const chatId = getChatId(data);
|
||||
const messageId = getMessageId(data);
|
||||
|
||||
if (chatId) {
|
||||
client.postMessage({
|
||||
type: 'focusMessage',
|
||||
payload: {
|
||||
chatId,
|
||||
messageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (client.focus) {
|
||||
client.focus();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const { data } = e.notification;
|
||||
const notifyClients = async () => {
|
||||
const clients = await self.clients.matchAll({ type: 'window' }) as WindowClient[];
|
||||
const clientsInScope = clients.filter((client) => client.url === self.registration.scope);
|
||||
clientsInScope.forEach((client) => focusChatMessage(client, data));
|
||||
if (!self.clients.openWindow || clientsInScope.length > 0) return undefined;
|
||||
|
||||
// If there is no opened client we need to open one and wait until it is fully loaded
|
||||
const newClient = await self.clients.openWindow(appUrl);
|
||||
if (newClient) {
|
||||
// Store notification data until client is fully loaded
|
||||
clickBuffer[newClient.id] = data;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
e.waitUntil(notifyClients());
|
||||
}
|
||||
|
||||
export function handleClientMessage(e: ExtendableMessageEvent) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[SW] New message from client', e);
|
||||
}
|
||||
if (e.data && e.data.type === 'clientReady') {
|
||||
const source = e.source as WindowClient;
|
||||
|
||||
// focus on chat message when client is fully ready
|
||||
if (clickBuffer[source.id]) {
|
||||
focusChatMessage(source, clickBuffer[source.id]);
|
||||
delete clickBuffer[source.id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { callApi } from '../api/gramjs';
|
||||
import { DEBUG } from '../config';
|
||||
import { getDispatch, getGlobal } from '../lib/teact/teactn';
|
||||
import { IS_SERVICE_WORKER_SUPPORTED } from './environment';
|
||||
|
||||
function getDeviceToken(subscription: PushSubscription) {
|
||||
@ -7,7 +8,7 @@ function getDeviceToken(subscription: PushSubscription) {
|
||||
return JSON.stringify({ endpoint: data.endpoint, keys: data.keys });
|
||||
}
|
||||
|
||||
export function isPushSupported() {
|
||||
function checkIfSupported() {
|
||||
if (!IS_SERVICE_WORKER_SUPPORTED) return false;
|
||||
if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
|
||||
if (DEBUG) {
|
||||
@ -39,19 +40,25 @@ export function isPushSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function unsubscribeFromPush() {
|
||||
if (!isPushSupported) return;
|
||||
const serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
||||
const subscription = await serviceWorkerRegistration.pushManager.getSubscription();
|
||||
const expirationTime = 12 * 60 * 60 * 1000; // 12 hours
|
||||
|
||||
function checkIfShouldResubscribe(subscription: PushSubscription | null) {
|
||||
const global = getGlobal();
|
||||
if (!global.push || !subscription) return true;
|
||||
if (getDeviceToken(subscription) !== global.push.deviceToken) return true;
|
||||
return Date.now() - global.push.subscribedAt > expirationTime;
|
||||
}
|
||||
|
||||
async function unsubscribe(subscription: PushSubscription | null) {
|
||||
const global = getGlobal();
|
||||
const dispatch = getDispatch();
|
||||
if (subscription) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[PUSH] Unsubscribing', subscription);
|
||||
}
|
||||
try {
|
||||
const deviceToken = getDeviceToken(subscription);
|
||||
await callApi('unregisterDevice', deviceToken);
|
||||
await subscription.unsubscribe();
|
||||
dispatch.deleteDeviceToken();
|
||||
return;
|
||||
} catch (error) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -59,14 +66,27 @@ export async function unsubscribeFromPush() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (global.push) {
|
||||
await callApi('unregisterDevice', global.push.deviceToken);
|
||||
dispatch.deleteDeviceToken();
|
||||
}
|
||||
}
|
||||
|
||||
export async function unsubscribeFromPush() {
|
||||
if (!checkIfSupported()) return;
|
||||
const serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
||||
const subscription = await serviceWorkerRegistration.pushManager.getSubscription();
|
||||
await unsubscribe(subscription);
|
||||
}
|
||||
|
||||
export async function subscribeToPush() {
|
||||
if (!isPushSupported()) return;
|
||||
await unsubscribeFromPush();
|
||||
if (!checkIfSupported()) return;
|
||||
const serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
||||
let subscription = await serviceWorkerRegistration.pushManager.getSubscription();
|
||||
if (!checkIfShouldResubscribe(subscription)) return;
|
||||
await unsubscribe(subscription);
|
||||
try {
|
||||
const subscription = await serviceWorkerRegistration.pushManager.subscribe({
|
||||
subscription = await serviceWorkerRegistration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
});
|
||||
const deviceToken = getDeviceToken(subscription);
|
||||
@ -75,6 +95,7 @@ export async function subscribeToPush() {
|
||||
console.log('[PUSH] Received push subscription: ', deviceToken);
|
||||
}
|
||||
await callApi('registerDevice', deviceToken);
|
||||
getDispatch().setDeviceToken(deviceToken);
|
||||
} catch (error) {
|
||||
if (Notification.permission === 'denied' as NotificationPermission) {
|
||||
// The user denied the notification permission which
|
||||
@ -94,3 +115,11 @@ export async function subscribeToPush() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify service worker that client is fully loaded
|
||||
export function notifyClientReady() {
|
||||
if (!navigator.serviceWorker.controller) return;
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'clientReady',
|
||||
});
|
||||
}
|
||||
|
@ -4,6 +4,27 @@ import { DEBUG } from '../config';
|
||||
import { getDispatch } from '../lib/teact/teactn';
|
||||
import { IS_SERVICE_WORKER_SUPPORTED } from './environment';
|
||||
|
||||
type WorkerAction = {
|
||||
type: string;
|
||||
payload: Record<string, any>;
|
||||
};
|
||||
|
||||
|
||||
function handleWorkerMessage(e: MessageEvent) {
|
||||
const action:WorkerAction = e.data;
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[SW] New action from service worker', action);
|
||||
}
|
||||
if (!action.type) return;
|
||||
const dispatch = getDispatch();
|
||||
switch (action.type) {
|
||||
case 'focusMessage':
|
||||
dispatch.focusMessage(action.payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (IS_SERVICE_WORKER_SUPPORTED) {
|
||||
window.addEventListener('load', async () => {
|
||||
try {
|
||||
@ -11,7 +32,7 @@ if (IS_SERVICE_WORKER_SUPPORTED) {
|
||||
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ServiceWorker registered');
|
||||
console.log('[SW] ServiceWorker registered');
|
||||
}
|
||||
|
||||
await navigator.serviceWorker.ready;
|
||||
@ -19,19 +40,21 @@ if (IS_SERVICE_WORKER_SUPPORTED) {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ServiceWorker ready');
|
||||
console.log('[SW] ServiceWorker ready');
|
||||
}
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', handleWorkerMessage);
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('ServiceWorker not available');
|
||||
console.error('[SW] ServiceWorker not available');
|
||||
}
|
||||
getDispatch().showError({ error: { message: 'SERVICE_WORKER_DISABLED' } });
|
||||
}
|
||||
} catch (err) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('ServiceWorker registration failed: ', err);
|
||||
console.error('[SW] ServiceWorker registration failed: ', err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user