Push notifications
This commit is contained in:
parent
e773402cbf
commit
9e320ea136
@ -313,6 +313,12 @@ console.timeEnd('get storage1'); */
|
|||||||
scrollable.append(placeholder.cloneNode());
|
scrollable.append(placeholder.cloneNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
(await import('./lib/mtproto/webPushApiManager')).default.forceUnsubscribe();
|
||||||
|
} catch(err) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
let pagePromise: Promise<void>;
|
let pagePromise: Promise<void>;
|
||||||
//langPromise.then(async() => {
|
//langPromise.then(async() => {
|
||||||
switch(authState._) {
|
switch(authState._) {
|
||||||
|
@ -117,6 +117,12 @@ const lang = {
|
|||||||
"other_value": "Forwarded %d messages"
|
"other_value": "Forwarded %d messages"
|
||||||
},
|
},
|
||||||
"Notifications.New": "New notification",
|
"Notifications.New": "New notification",
|
||||||
|
"PushNotification.Action.Mute1d": "Mute background alerts for 1 day",
|
||||||
|
"PushNotification.Action.Settings": "Background alerts settings",
|
||||||
|
"PushNotification.Action.Mute1d.Mobile": "Mute for 24H",
|
||||||
|
"PushNotification.Action.Settings.Mobile": "Alerts settings",
|
||||||
|
"PushNotification.Message.NoPreview": "You have a new message",
|
||||||
|
//"PushNotification.Action.Mute1d.Success": "Notification settings were successfully saved.",
|
||||||
|
|
||||||
// * android
|
// * android
|
||||||
"ActionCreateChannel": "Channel created",
|
"ActionCreateChannel": "Channel created",
|
||||||
|
@ -171,7 +171,11 @@ export class AppImManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rootScope.addEventListener('history_focus', (e) => {
|
rootScope.addEventListener('history_focus', (e) => {
|
||||||
const {peerId, mid} = e;
|
let {peerId, mid} = e;
|
||||||
|
if(mid) {
|
||||||
|
mid = appMessagesManager.generateMessageId(mid); // because mid can come from notification, i.e. server message id
|
||||||
|
}
|
||||||
|
|
||||||
this.setInnerPeer(peerId, mid);
|
this.setInnerPeer(peerId, mid);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -204,8 +208,7 @@ export class AppImManager {
|
|||||||
(popup as any).onClose = () => {
|
(popup as any).onClose = () => {
|
||||||
document.body.classList.add('deactivated-backwards');
|
document.body.classList.add('deactivated-backwards');
|
||||||
|
|
||||||
singleInstance.reset();
|
singleInstance.activateInstance();
|
||||||
singleInstance.checkInstance(false);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.classList.remove('deactivated', 'deactivated-backwards');
|
document.body.classList.remove('deactivated', 'deactivated-backwards');
|
||||||
|
@ -19,11 +19,14 @@ import { isMobile } from "../../helpers/userAgent";
|
|||||||
import { InputNotifyPeer, InputPeerNotifySettings, NotifyPeer, PeerNotifySettings, Update } from "../../layer";
|
import { InputNotifyPeer, InputPeerNotifySettings, NotifyPeer, PeerNotifySettings, Update } from "../../layer";
|
||||||
import I18n from "../langPack";
|
import I18n from "../langPack";
|
||||||
import apiManager from "../mtproto/mtprotoworker";
|
import apiManager from "../mtproto/mtprotoworker";
|
||||||
|
import webPushApiManager, { PushSubscriptionNotify } from "../mtproto/webPushApiManager";
|
||||||
import rootScope from "../rootScope";
|
import rootScope from "../rootScope";
|
||||||
import stateStorage from "../stateStorage";
|
import stateStorage from "../stateStorage";
|
||||||
import apiUpdatesManager from "./apiUpdatesManager";
|
import apiUpdatesManager from "./apiUpdatesManager";
|
||||||
|
import appChatsManager from "./appChatsManager";
|
||||||
import appPeersManager from "./appPeersManager";
|
import appPeersManager from "./appPeersManager";
|
||||||
import appStateManager from "./appStateManager";
|
import appStateManager from "./appStateManager";
|
||||||
|
import appUsersManager from "./appUsersManager";
|
||||||
|
|
||||||
type MyNotification = Notification & {
|
type MyNotification = Notification & {
|
||||||
hidden?: boolean,
|
hidden?: boolean,
|
||||||
@ -40,6 +43,15 @@ export type NotifyOptions = Partial<{
|
|||||||
onclick: () => void;
|
onclick: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type NotificationSettings = {
|
||||||
|
nodesktop: boolean,
|
||||||
|
volume: number,
|
||||||
|
novibrate: boolean,
|
||||||
|
nopreview: boolean,
|
||||||
|
nopush: boolean,
|
||||||
|
nosound: boolean
|
||||||
|
};
|
||||||
|
|
||||||
type ImSadAboutIt = Promise<PeerNotifySettings> | PeerNotifySettings;
|
type ImSadAboutIt = Promise<PeerNotifySettings> | PeerNotifySettings;
|
||||||
export class AppNotificationsManager {
|
export class AppNotificationsManager {
|
||||||
private notificationsUiSupport: boolean;
|
private notificationsUiSupport: boolean;
|
||||||
@ -66,14 +78,7 @@ export class AppNotificationsManager {
|
|||||||
private prevFavicon: string;
|
private prevFavicon: string;
|
||||||
private stopped = false;
|
private stopped = false;
|
||||||
|
|
||||||
private settings: Partial<{
|
private settings: NotificationSettings = {} as any;
|
||||||
nodesktop: boolean,
|
|
||||||
volume: number,
|
|
||||||
novibrate: boolean,
|
|
||||||
nopreview: boolean,
|
|
||||||
nopush: boolean,
|
|
||||||
nosound: boolean,
|
|
||||||
}> = {};
|
|
||||||
|
|
||||||
private registeredDevice: any;
|
private registeredDevice: any;
|
||||||
private pushInited = false;
|
private pushInited = false;
|
||||||
@ -96,11 +101,15 @@ export class AppNotificationsManager {
|
|||||||
this.notifySoundEl.id = 'notify-sound';
|
this.notifySoundEl.id = 'notify-sound';
|
||||||
document.body.append(this.notifySoundEl);
|
document.body.append(this.notifySoundEl);
|
||||||
|
|
||||||
/* rootScope.on('idle.deactivated', (newVal) => {
|
rootScope.addEventListener('instance_deactivated', () => {
|
||||||
if(newVal) {
|
this.stop();
|
||||||
stop();
|
});
|
||||||
|
|
||||||
|
rootScope.addEventListener('instance_activated', () => {
|
||||||
|
if(this.stopped) {
|
||||||
|
this.start();
|
||||||
}
|
}
|
||||||
});*/
|
});
|
||||||
|
|
||||||
rootScope.addEventListener('idle', (newVal) => {
|
rootScope.addEventListener('idle', (newVal) => {
|
||||||
if(this.stopped) {
|
if(this.stopped) {
|
||||||
@ -121,40 +130,40 @@ export class AppNotificationsManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* rootScope.on('push_init', (tokenData) => {
|
rootScope.addEventListener('push_init', (tokenData) => {
|
||||||
this.pushInited = true
|
this.pushInited = true;
|
||||||
if(!this.settings.nodesktop && !this.settings.nopush) {
|
if(!this.settings.nodesktop && !this.settings.nopush) {
|
||||||
if(tokenData) {
|
if(tokenData) {
|
||||||
this.registerDevice(tokenData);
|
this.registerDevice(tokenData);
|
||||||
} else {
|
} else {
|
||||||
WebPushApiManager.subscribe();
|
webPushApiManager.subscribe();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.unregisterDevice(tokenData);
|
this.unregisterDevice(tokenData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
rootScope.on('push_subscribe', (tokenData) => {
|
rootScope.addEventListener('push_subscribe', (tokenData) => {
|
||||||
this.registerDevice(tokenData);
|
this.registerDevice(tokenData);
|
||||||
});
|
});
|
||||||
rootScope.on('push_unsubscribe', (tokenData) => {
|
rootScope.addEventListener('push_unsubscribe', (tokenData) => {
|
||||||
this.unregisterDevice(tokenData);
|
this.unregisterDevice(tokenData);
|
||||||
}); */
|
});
|
||||||
|
|
||||||
rootScope.addEventListener('dialogs_multiupdate', () => {
|
rootScope.addEventListener('dialogs_multiupdate', () => {
|
||||||
//unregisterTopMsgs()
|
//unregisterTopMsgs()
|
||||||
this.topMessagesDeferred.resolve();
|
this.topMessagesDeferred.resolve();
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
/* rootScope.on('push_notification_click', (notificationData) => {
|
rootScope.addEventListener('push_notification_click', (notificationData) => {
|
||||||
if(notificationData.action === 'push_settings') {
|
if(notificationData.action === 'push_settings') {
|
||||||
this.topMessagesDeferred.then(() => {
|
/* this.topMessagesDeferred.then(() => {
|
||||||
$modal.open({
|
$modal.open({
|
||||||
templateUrl: templateUrl('settings_modal'),
|
templateUrl: templateUrl('settings_modal'),
|
||||||
controller: 'SettingsModalController',
|
controller: 'SettingsModalController',
|
||||||
windowClass: 'settings_modal_window mobile_modal',
|
windowClass: 'settings_modal_window mobile_modal',
|
||||||
backdrop: 'single'
|
backdrop: 'single'
|
||||||
})
|
})
|
||||||
});
|
}); */
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,12 +185,12 @@ export class AppNotificationsManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const peerId = notificationData.custom && notificationData.custom.peerId;
|
const peerId = notificationData.custom && +notificationData.custom.peerId;
|
||||||
console.log('click', notificationData, peerId);
|
console.log('click', notificationData, peerId);
|
||||||
if(peerId) {
|
if(peerId) {
|
||||||
this.topMessagesDeferred.then(() => {
|
this.topMessagesDeferred.then(() => {
|
||||||
if(notificationData.custom.channel_id &&
|
if(notificationData.custom.channel_id &&
|
||||||
!appChatsManager.hasChat(notificationData.custom.channel_id)) {
|
!appChatsManager.hasChat(+notificationData.custom.channel_id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,12 +198,13 @@ export class AppNotificationsManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// rootScope.broadcast('history_focus', {
|
rootScope.dispatchEvent('history_focus', {
|
||||||
// peerString: appPeersManager.getPeerString(peerId)
|
peerId,
|
||||||
// });
|
mid: +notificationData.custom.msg_id
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}); */
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleToggler(enable = rootScope.idle.isIDLE) {
|
private toggleToggler(enable = rootScope.idle.isIDLE) {
|
||||||
@ -276,19 +286,19 @@ export class AppNotificationsManager {
|
|||||||
this.settings.nopreview = updSettings[3];
|
this.settings.nopreview = updSettings[3];
|
||||||
this.settings.nopush = updSettings[4];
|
this.settings.nopush = updSettings[4];
|
||||||
|
|
||||||
/* if(this.pushInited) {
|
if(this.pushInited) {
|
||||||
const needPush = !this.settings.nopush && !this.settings.nodesktop && WebPushApiManager.isAvailable || false;
|
const needPush = !this.settings.nopush && !this.settings.nodesktop && webPushApiManager.isAvailable || false;
|
||||||
const hasPush = this.registeredDevice !== false;
|
const hasPush = this.registeredDevice !== false;
|
||||||
if(needPush !== hasPush) {
|
if(needPush !== hasPush) {
|
||||||
if(needPush) {
|
if(needPush) {
|
||||||
WebPushApiManager.subscribe();
|
webPushApiManager.subscribe();
|
||||||
} else {
|
} else {
|
||||||
WebPushApiManager.unsubscribe();
|
webPushApiManager.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WebPushApiManager.setSettings(this.settings); */
|
webPushApiManager.setSettings(this.settings);
|
||||||
});
|
});
|
||||||
|
|
||||||
appStateManager.getState().then(state => {
|
appStateManager.getState().then(state => {
|
||||||
@ -457,7 +467,7 @@ export class AppNotificationsManager {
|
|||||||
public start() {
|
public start() {
|
||||||
this.updateLocalSettings();
|
this.updateLocalSettings();
|
||||||
rootScope.addEventListener('settings_updated', this.updateLocalSettings);
|
rootScope.addEventListener('settings_updated', this.updateLocalSettings);
|
||||||
//WebPushApiManager.start();
|
webPushApiManager.start();
|
||||||
|
|
||||||
if(!this.notificationsUiSupport) {
|
if(!this.notificationsUiSupport) {
|
||||||
return false;
|
return false;
|
||||||
@ -571,7 +581,7 @@ export class AppNotificationsManager {
|
|||||||
//console.log('notify constructed notification');
|
//console.log('notify constructed notification');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.notificationsUiSupport = false;
|
this.notificationsUiSupport = false;
|
||||||
//WebPushApiManager.setLocalNotificationsDisabled();
|
webPushApiManager.setLocalNotificationsDisabled();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} /* else if('mozNotification' in navigator) {
|
} /* else if('mozNotification' in navigator) {
|
||||||
@ -692,12 +702,11 @@ export class AppNotificationsManager {
|
|||||||
this.notificationsShown = {};
|
this.notificationsShown = {};
|
||||||
this.notificationsCount = 0;
|
this.notificationsCount = 0;
|
||||||
|
|
||||||
//WebPushApiManager.hidePushNotifications();
|
webPushApiManager.hidePushNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerDevice(tokenData: any) {
|
private registerDevice(tokenData: PushSubscriptionNotify) {
|
||||||
if(this.registeredDevice &&
|
if(this.registeredDevice && deepEqual(this.registeredDevice, tokenData)) {
|
||||||
deepEqual(this.registeredDevice, tokenData)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -711,10 +720,10 @@ export class AppNotificationsManager {
|
|||||||
this.registeredDevice = tokenData;
|
this.registeredDevice = tokenData;
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
error.handled = true;
|
error.handled = true;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private unregisterDevice(tokenData: any) {
|
private unregisterDevice(tokenData: PushSubscriptionNotify) {
|
||||||
if(!this.registeredDevice) {
|
if(!this.registeredDevice) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -724,10 +733,10 @@ export class AppNotificationsManager {
|
|||||||
token: tokenData.tokenValue,
|
token: tokenData.tokenValue,
|
||||||
other_uids: []
|
other_uids: []
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.registeredDevice = false
|
this.registeredDevice = false;
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
error.handled = true
|
error.handled = true;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getVibrateSupport() {
|
public getVibrateSupport() {
|
||||||
|
@ -157,7 +157,7 @@ export class ApiManager {
|
|||||||
storageKeys.push(prefix + dcId + '_auth_key');
|
storageKeys.push(prefix + dcId + '_auth_key');
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebPushApiManager.forceUnsubscribe(); // WARNING
|
// WebPushApiManager.forceUnsubscribe(); // WARNING // moved to worker's master
|
||||||
const storageResult = await Promise.all(storageKeys.map(key => sessionStorage.get(key as any)));
|
const storageResult = await Promise.all(storageKeys.map(key => sessionStorage.get(key as any)));
|
||||||
|
|
||||||
const logoutPromises: Promise<any>[] = [];
|
const logoutPromises: Promise<any>[] = [];
|
||||||
|
@ -1,286 +0,0 @@
|
|||||||
/*
|
|
||||||
* https://github.com/morethanwords/tweb
|
|
||||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
||||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// #if MTPROTO_SW
|
|
||||||
import './mtproto.worker';
|
|
||||||
/// #endif
|
|
||||||
//import CacheStorageController from '../cacheStorage';
|
|
||||||
import { isSafari } from '../../helpers/userAgent';
|
|
||||||
import { logger, LogTypes } from '../logger';
|
|
||||||
import type { DownloadOptions } from './apiFileManager';
|
|
||||||
import type { WorkerTaskTemplate } from '../../types';
|
|
||||||
import { notifySomeone } from '../../helpers/context';
|
|
||||||
import type { InputFileLocation, UploadFile } from '../../layer';
|
|
||||||
import { CancellablePromise, deferredPromise } from '../../helpers/cancellablePromise';
|
|
||||||
|
|
||||||
const log = logger('SW', LogTypes.Error | LogTypes.Debug | LogTypes.Log | LogTypes.Warn);
|
|
||||||
const ctx = self as any as ServiceWorkerGlobalScope;
|
|
||||||
|
|
||||||
const deferredPromises: {[taskId: number]: CancellablePromise<any>} = {};
|
|
||||||
|
|
||||||
export interface ServiceWorkerTask extends WorkerTaskTemplate {
|
|
||||||
type: 'requestFilePart',
|
|
||||||
payload: [number, InputFileLocation, number, number]
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ServiceWorkerTaskResponse extends WorkerTaskTemplate {
|
|
||||||
type: 'requestFilePart',
|
|
||||||
payload?: UploadFile.uploadFile,
|
|
||||||
originalPayload?: ServiceWorkerTask['payload']
|
|
||||||
};
|
|
||||||
|
|
||||||
/// #if !MTPROTO_SW
|
|
||||||
ctx.addEventListener('message', (e) => {
|
|
||||||
const task = e.data as ServiceWorkerTaskResponse;
|
|
||||||
const promise = deferredPromises[task.id];
|
|
||||||
|
|
||||||
if(task.error) {
|
|
||||||
promise.reject(task.error);
|
|
||||||
} else {
|
|
||||||
promise.resolve(task.payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete deferredPromises[task.id];
|
|
||||||
});
|
|
||||||
/// #endif
|
|
||||||
|
|
||||||
//const cacheStorage = new CacheStorageController('cachedAssets');
|
|
||||||
let taskId = 0;
|
|
||||||
|
|
||||||
function isCorrectResponse(response: Response) {
|
|
||||||
return response.ok && response.status === 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestCache(event: FetchEvent) {
|
|
||||||
try {
|
|
||||||
const cache = await ctx.caches.open('cachedAssets');
|
|
||||||
const file = await cache.match(event.request, {ignoreVary: true});
|
|
||||||
|
|
||||||
if(file && isCorrectResponse(file)) {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers: HeadersInit = {'Vary': '*'};
|
|
||||||
let response = await fetch(event.request, {headers});
|
|
||||||
if(isCorrectResponse(response)) {
|
|
||||||
cache.put(event.request, response.clone());
|
|
||||||
} else if(response.status === 304) { // possible fix for 304 in Safari
|
|
||||||
const url = event.request.url.replace(/\?.+$/, '') + '?' + (Math.random() * 100000 | 0);
|
|
||||||
response = await fetch(url, {headers});
|
|
||||||
if(isCorrectResponse(response)) {
|
|
||||||
cache.put(event.request, response.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch(err) {
|
|
||||||
return fetch(event.request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFetch = (event: FetchEvent): void => {
|
|
||||||
if(event.request.url.indexOf(location.origin + '/') === 0 && event.request.url.match(/\.(js|css|jpe?g|json|wasm|png|mp3|svg|tgs|ico|woff2?|ttf|webmanifest?)(?:\?.*)?$/)) {
|
|
||||||
return event.respondWith(requestCache(event));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || [];
|
|
||||||
|
|
||||||
//log.debug('[fetch]:', event);
|
|
||||||
|
|
||||||
switch(scope) {
|
|
||||||
case 'stream': {
|
|
||||||
const range = parseRange(event.request.headers.get('Range'));
|
|
||||||
let [offset, end] = range;
|
|
||||||
|
|
||||||
const info: DownloadOptions = JSON.parse(decodeURIComponent(params));
|
|
||||||
//const fileName = getFileNameByLocation(info.location);
|
|
||||||
|
|
||||||
// ! если грузить очень большое видео чанками по 512Кб в мобильном Safari, то стрим не запустится
|
|
||||||
const limitPart = info.size > (75 * 1024 * 1024) ? STREAM_CHUNK_UPPER_LIMIT : STREAM_CHUNK_MIDDLE_LIMIT;
|
|
||||||
|
|
||||||
/* if(info.size > limitPart && isSafari && offset === limitPart) {
|
|
||||||
//end = info.size - 1;
|
|
||||||
//offset = info.size - 1 - limitPart;
|
|
||||||
offset = info.size - (info.size % limitPart);
|
|
||||||
} */
|
|
||||||
|
|
||||||
//log.debug('[stream]', url, offset, end);
|
|
||||||
|
|
||||||
event.respondWith(Promise.race([
|
|
||||||
timeout(45 * 1000),
|
|
||||||
|
|
||||||
new Promise<Response>((resolve, reject) => {
|
|
||||||
// safari workaround
|
|
||||||
const possibleResponse = responseForSafariFirstRange(range, info.mimeType, info.size);
|
|
||||||
if(possibleResponse) {
|
|
||||||
return resolve(possibleResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
const limit = end && end < limitPart ? alignLimit(end - offset + 1) : limitPart;
|
|
||||||
const alignedOffset = alignOffset(offset, limit);
|
|
||||||
|
|
||||||
//log.debug('[stream] requestFilePart:', /* info.dcId, info.location, */ alignedOffset, limit);
|
|
||||||
|
|
||||||
const task: ServiceWorkerTask = {
|
|
||||||
type: 'requestFilePart',
|
|
||||||
id: taskId++,
|
|
||||||
payload: [info.dcId, info.location, alignedOffset, limit]
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const deferred = deferredPromises[task.id] = deferredPromise<UploadFile.uploadFile>();
|
|
||||||
deferred.then(result => {
|
|
||||||
let ab = result.bytes as Uint8Array;
|
|
||||||
|
|
||||||
//log.debug('[stream] requestFilePart result:', result);
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Accept-Ranges': 'bytes',
|
|
||||||
'Content-Range': `bytes ${alignedOffset}-${alignedOffset + ab.byteLength - 1}/${info.size || '*'}`,
|
|
||||||
'Content-Length': `${ab.byteLength}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
if(info.mimeType) headers['Content-Type'] = info.mimeType;
|
|
||||||
|
|
||||||
if(isSafari) {
|
|
||||||
ab = ab.slice(offset - alignedOffset, end - alignedOffset + 1);
|
|
||||||
headers['Content-Range'] = `bytes ${offset}-${offset + ab.byteLength - 1}/${info.size || '*'}`;
|
|
||||||
headers['Content-Length'] = `${ab.byteLength}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// simulate slow connection
|
|
||||||
//setTimeout(() => {
|
|
||||||
resolve(new Response(ab, {
|
|
||||||
status: 206,
|
|
||||||
statusText: 'Partial Content',
|
|
||||||
headers,
|
|
||||||
}));
|
|
||||||
//}, 2.5e3);
|
|
||||||
}).catch(err => {});
|
|
||||||
|
|
||||||
notifySomeone(task);
|
|
||||||
})
|
|
||||||
]));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(err) {
|
|
||||||
event.respondWith(new Response('', {
|
|
||||||
status: 500,
|
|
||||||
statusText: 'Internal Server Error',
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChangeState = () => {
|
|
||||||
ctx.onfetch = onFetch;
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.addEventListener('install', (event) => {
|
|
||||||
log('installing');
|
|
||||||
|
|
||||||
/* initCache();
|
|
||||||
|
|
||||||
event.waitUntil(
|
|
||||||
initNetwork(),
|
|
||||||
); */
|
|
||||||
event.waitUntil(ctx.skipWaiting()); // Activate worker immediately
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.addEventListener('activate', (event) => {
|
|
||||||
log('activating', ctx);
|
|
||||||
|
|
||||||
/* if (!ctx.cache) initCache();
|
|
||||||
if (!ctx.network) initNetwork(); */
|
|
||||||
|
|
||||||
event.waitUntil(ctx.caches.delete('cachedAssets'));
|
|
||||||
event.waitUntil(ctx.clients.claim());
|
|
||||||
});
|
|
||||||
|
|
||||||
function timeout(delay: number): Promise<Response> {
|
|
||||||
return new Promise(((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve(new Response('', {
|
|
||||||
status: 408,
|
|
||||||
statusText: 'Request timed out.',
|
|
||||||
}));
|
|
||||||
}, delay);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function responseForSafariFirstRange(range: [number, number], mimeType: string, size: number): Response {
|
|
||||||
if(range[0] === 0 && range[1] === 1) {
|
|
||||||
return new Response(new Uint8Array(2).buffer, {
|
|
||||||
status: 206,
|
|
||||||
statusText: 'Partial Content',
|
|
||||||
headers: {
|
|
||||||
'Accept-Ranges': 'bytes',
|
|
||||||
'Content-Range': `bytes 0-1/${size || '*'}`,
|
|
||||||
'Content-Length': '2',
|
|
||||||
'Content-Type': mimeType || 'video/mp4',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.onerror = (error) => {
|
|
||||||
log.error('error:', error);
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.onunhandledrejection = (error) => {
|
|
||||||
log.error('onunhandledrejection:', error);
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.onoffline = ctx.ononline = onChangeState;
|
|
||||||
|
|
||||||
onChangeState();
|
|
||||||
|
|
||||||
/* const STREAM_CHUNK_UPPER_LIMIT = 256 * 1024;
|
|
||||||
const SMALLEST_CHUNK_LIMIT = 256 * 4; */
|
|
||||||
/* const STREAM_CHUNK_UPPER_LIMIT = 1024 * 1024;
|
|
||||||
const SMALLEST_CHUNK_LIMIT = 1024 * 4; */
|
|
||||||
const STREAM_CHUNK_MIDDLE_LIMIT = 512 * 1024;
|
|
||||||
const STREAM_CHUNK_UPPER_LIMIT = 1024 * 1024;
|
|
||||||
const SMALLEST_CHUNK_LIMIT = 512 * 4;
|
|
||||||
|
|
||||||
function parseRange(header: string): [number, number] {
|
|
||||||
if(!header) return [0, 0];
|
|
||||||
const [, chunks] = header.split('=');
|
|
||||||
const ranges = chunks.split(', ');
|
|
||||||
const [offset, end] = ranges[0].split('-');
|
|
||||||
|
|
||||||
return [+offset, +end || 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function alignOffset(offset: number, base = SMALLEST_CHUNK_LIMIT) {
|
|
||||||
return offset - (offset % base);
|
|
||||||
}
|
|
||||||
|
|
||||||
function alignLimit(limit: number) {
|
|
||||||
return 2 ** Math.ceil(Math.log(limit) / Math.log(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ctx.addEventListener('push', (event) => {
|
|
||||||
console.log('[Service Worker] Push Received.');
|
|
||||||
console.log(`[Service Worker] Push had this data: "${event.data.text()}"`, event, event.data);
|
|
||||||
|
|
||||||
const title = 'Push Push Push';
|
|
||||||
const options = {};
|
|
||||||
// const options = {
|
|
||||||
// body: 'Yay it works.',
|
|
||||||
// icon: 'images/icon.png',
|
|
||||||
// badge: 'images/badge.png'
|
|
||||||
// };
|
|
||||||
|
|
||||||
event.waitUntil(ctx.registration.showNotification(title, options));
|
|
||||||
}); */
|
|
||||||
|
|
||||||
//export default () => {};
|
|
||||||
|
|
||||||
//MOUNT_CLASS_TO.onFetch = onFetch;
|
|
@ -11,7 +11,7 @@ import apiManager from "./apiManager";
|
|||||||
import cryptoWorker from "../crypto/cryptoworker";
|
import cryptoWorker from "../crypto/cryptoworker";
|
||||||
import networkerFactory from "./networkerFactory";
|
import networkerFactory from "./networkerFactory";
|
||||||
import apiFileManager from './apiFileManager';
|
import apiFileManager from './apiFileManager';
|
||||||
import type { ServiceWorkerTask, ServiceWorkerTaskResponse } from './mtproto.service';
|
import type { RequestFilePartTask, RequestFilePartTaskResponse } from '../serviceWorker/index.service';
|
||||||
import { ctx } from '../../helpers/userAgent';
|
import { ctx } from '../../helpers/userAgent';
|
||||||
import { socketsProxied } from './dcConfigurator';
|
import { socketsProxied } from './dcConfigurator';
|
||||||
import { notifyAll } from '../../helpers/context';
|
import { notifyAll } from '../../helpers/context';
|
||||||
@ -44,8 +44,8 @@ const taskListeners = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
requestFilePart: async(task: ServiceWorkerTask) => {
|
requestFilePart: async(task: RequestFilePartTask) => {
|
||||||
const responseTask: ServiceWorkerTaskResponse = {
|
const responseTask: RequestFilePartTaskResponse = {
|
||||||
type: task.type,
|
type: task.type,
|
||||||
id: task.id
|
id: task.id
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,7 @@ import { logger } from '../logger';
|
|||||||
import rootScope from '../rootScope';
|
import rootScope from '../rootScope';
|
||||||
import webpWorkerController from '../webp/webpWorkerController';
|
import webpWorkerController from '../webp/webpWorkerController';
|
||||||
import type { DownloadOptions } from './apiFileManager';
|
import type { DownloadOptions } from './apiFileManager';
|
||||||
import type { ServiceWorkerTask } from './mtproto.service';
|
import type { ServiceWorkerTask } from '../serviceWorker/index.service';
|
||||||
import { UserAuth } from './mtproto_config';
|
import { UserAuth } from './mtproto_config';
|
||||||
import type { MTMessage } from './networker';
|
import type { MTMessage } from './networker';
|
||||||
import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug';
|
import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug';
|
||||||
@ -24,6 +24,7 @@ import Socket from './transports/websocket';
|
|||||||
import IDBStorage from '../idb';
|
import IDBStorage from '../idb';
|
||||||
import singleInstance from './singleInstance';
|
import singleInstance from './singleInstance';
|
||||||
import sessionStorage from '../sessionStorage';
|
import sessionStorage from '../sessionStorage';
|
||||||
|
import webPushApiManager from './webPushApiManager';
|
||||||
|
|
||||||
type Task = {
|
type Task = {
|
||||||
taskId: number,
|
taskId: number,
|
||||||
@ -83,6 +84,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
|
|||||||
private sockets: Map<number, Socket> = new Map();
|
private sockets: Map<number, Socket> = new Map();
|
||||||
|
|
||||||
private taskListeners: {[taskType: string]: (task: any) => void} = {};
|
private taskListeners: {[taskType: string]: (task: any) => void} = {};
|
||||||
|
private taskListenersSW: {[taskType: string]: (task: any) => void} = {};
|
||||||
|
|
||||||
public onServiceWorkerFail: () => void;
|
public onServiceWorkerFail: () => void;
|
||||||
|
|
||||||
@ -97,7 +99,8 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
|
|||||||
this.addTaskListener('clear', () => {
|
this.addTaskListener('clear', () => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
IDBStorage.deleteDatabase(),
|
IDBStorage.deleteDatabase(),
|
||||||
sessionStorage.clear()
|
sessionStorage.clear(),
|
||||||
|
webPushApiManager.forceUnsubscribe()
|
||||||
]).finally(() => {
|
]).finally(() => {
|
||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
@ -236,6 +239,13 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const callback = this.taskListenersSW[task.type];
|
||||||
|
if(callback) {
|
||||||
|
callback(task);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addServiceWorkerTaskListener('requestFilePart', (task) => {
|
||||||
this.postMessage(task);
|
this.postMessage(task);
|
||||||
});
|
});
|
||||||
/// #endif
|
/// #endif
|
||||||
@ -264,6 +274,10 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
|
|||||||
this.taskListeners[name] = callback;
|
this.taskListeners[name] = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addServiceWorkerTaskListener(name: keyof ApiManagerProxy['taskListenersSW'], callback: ApiManagerProxy['taskListenersSW'][typeof name]) {
|
||||||
|
this.taskListenersSW[name] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
private onWorkerMessage = (e: MessageEvent) => {
|
private onWorkerMessage = (e: MessageEvent) => {
|
||||||
//this.log('got message from worker:', e.data);
|
//this.log('got message from worker:', e.data);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ServiceWorkerTask, ServiceWorkerTaskResponse } from "./mtproto.service";
|
import type { RequestFilePartTask, RequestFilePartTaskResponse } from "../serviceWorker/index.service";
|
||||||
import type { ApiError } from "./apiManager";
|
import type { ApiError } from "./apiManager";
|
||||||
import appMessagesManager from "../appManagers/appMessagesManager";
|
import appMessagesManager from "../appManagers/appMessagesManager";
|
||||||
import { Photo } from "../../layer";
|
import { Photo } from "../../layer";
|
||||||
@ -38,7 +38,7 @@ class ReferenceDatabase {
|
|||||||
private links: {[hex: string]: ReferenceBytes} = {};
|
private links: {[hex: string]: ReferenceBytes} = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
apiManager.addTaskListener('requestFilePart', (task: ServiceWorkerTaskResponse) => {
|
apiManager.addTaskListener('requestFilePart', (task: RequestFilePartTaskResponse) => {
|
||||||
if(task.error) {
|
if(task.error) {
|
||||||
const onError = (error: ApiError) => {
|
const onError = (error: ApiError) => {
|
||||||
if(error?.type === 'FILE_REFERENCE_EXPIRED') {
|
if(error?.type === 'FILE_REFERENCE_EXPIRED') {
|
||||||
@ -47,7 +47,7 @@ class ReferenceDatabase {
|
|||||||
referenceDatabase.refreshReference(bytes).then(() => {
|
referenceDatabase.refreshReference(bytes).then(() => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
task.originalPayload[1].file_reference = referenceDatabase.getReferenceByLink(bytes);
|
task.originalPayload[1].file_reference = referenceDatabase.getReferenceByLink(bytes);
|
||||||
const newTask: ServiceWorkerTask = {
|
const newTask: RequestFilePartTask = {
|
||||||
type: task.type,
|
type: task.type,
|
||||||
id: task.id,
|
id: task.id,
|
||||||
payload: task.originalPayload
|
payload: task.originalPayload
|
||||||
|
@ -1,3 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*
|
||||||
|
* Originally from:
|
||||||
|
* https://github.com/zhukov/webogram
|
||||||
|
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
|
||||||
|
* https://github.com/zhukov/webogram/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
import { MOUNT_CLASS_TO } from "../../config/debug";
|
import { MOUNT_CLASS_TO } from "../../config/debug";
|
||||||
import { nextRandomInt } from "../../helpers/random";
|
import { nextRandomInt } from "../../helpers/random";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
@ -57,6 +68,14 @@ export class SingleInstance {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public activateInstance() {
|
||||||
|
if(this.deactivated) {
|
||||||
|
this.reset();
|
||||||
|
this.checkInstance(false);
|
||||||
|
rootScope.dispatchEvent('instance_activated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public deactivateInstance = () => {
|
public deactivateInstance = () => {
|
||||||
if(this.masterInstance || this.deactivated) {
|
if(this.masterInstance || this.deactivated) {
|
||||||
return false;
|
return false;
|
||||||
|
254
src/lib/mtproto/webPushApiManager.ts
Normal file
254
src/lib/mtproto/webPushApiManager.ts
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*
|
||||||
|
* Originally from:
|
||||||
|
* https://github.com/zhukov/webogram
|
||||||
|
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
|
||||||
|
* https://github.com/zhukov/webogram/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NotificationSettings } from "../appManagers/appNotificationsManager";
|
||||||
|
import { MOUNT_CLASS_TO } from "../../config/debug";
|
||||||
|
import { copy } from "../../helpers/object";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import rootScope from "../rootScope";
|
||||||
|
import { ServiceWorkerNotificationsClearTask, ServiceWorkerPingTask, ServiceWorkerPushClickTask } from "../serviceWorker/index.service";
|
||||||
|
import apiManager from "./mtprotoworker";
|
||||||
|
import I18n, { LangPackKey } from "../langPack";
|
||||||
|
import { isMobile } from "../../helpers/userAgent";
|
||||||
|
|
||||||
|
export type PushSubscriptionNotifyType = 'init' | 'subscribe' | 'unsubscribe';
|
||||||
|
export type PushSubscriptionNotifyEvent = `push_${PushSubscriptionNotifyType}`;
|
||||||
|
|
||||||
|
export type PushSubscriptionNotify = {
|
||||||
|
tokenType: number,
|
||||||
|
tokenValue: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WebPushApiManager {
|
||||||
|
public isAvailable = true;
|
||||||
|
private isPushEnabled = false;
|
||||||
|
private localNotificationsAvailable = true;
|
||||||
|
private started = false;
|
||||||
|
private settings: NotificationSettings & {baseUrl?: string} = {} as any;
|
||||||
|
private isAliveTO: any;
|
||||||
|
private isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||||
|
private userVisibleOnly = this.isFirefox ? false : true;
|
||||||
|
private log = logger('PM');
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if(!('PushManager' in window) ||
|
||||||
|
!('Notification' in window) ||
|
||||||
|
!('serviceWorker' in navigator)) {
|
||||||
|
this.log.warn('Push messaging is not supported.');
|
||||||
|
this.isAvailable = false;
|
||||||
|
this.localNotificationsAvailable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.isAvailable && Notification.permission === 'denied') {
|
||||||
|
this.log.warn('The user has blocked notifications.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
if(!this.started) {
|
||||||
|
this.started = true;
|
||||||
|
this.getSubscription();
|
||||||
|
this.setUpServiceWorkerChannel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLocalNotificationsDisabled() {
|
||||||
|
this.localNotificationsAvailable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSubscription() {
|
||||||
|
if(!this.isAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.serviceWorker.ready.then((reg) => {
|
||||||
|
reg.pushManager.getSubscription().then((subscription) => {
|
||||||
|
this.isPushEnabled = !!subscription;
|
||||||
|
this.pushSubscriptionNotify('init', subscription);
|
||||||
|
}).catch((err) => {
|
||||||
|
this.log.error('Error during getSubscription()', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe = () => {
|
||||||
|
if(!this.isAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.serviceWorker.ready.then((reg) => {
|
||||||
|
reg.pushManager.subscribe({userVisibleOnly: this.userVisibleOnly}).then((subscription) => {
|
||||||
|
// The subscription was successful
|
||||||
|
this.isPushEnabled = true;
|
||||||
|
this.pushSubscriptionNotify('subscribe', subscription);
|
||||||
|
}).catch((e) => {
|
||||||
|
if(Notification.permission === 'denied') {
|
||||||
|
this.log('Permission for Notifications was denied');
|
||||||
|
} else {
|
||||||
|
this.log('Unable to subscribe to push.', e);
|
||||||
|
if(!this.userVisibleOnly) {
|
||||||
|
this.userVisibleOnly = true;
|
||||||
|
setTimeout(this.subscribe, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribe() {
|
||||||
|
if(!this.isAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.serviceWorker.ready.then((reg) => {
|
||||||
|
reg.pushManager.getSubscription().then((subscription) => {
|
||||||
|
this.isPushEnabled = false;
|
||||||
|
|
||||||
|
if(subscription) {
|
||||||
|
this.pushSubscriptionNotify('unsubscribe', subscription);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
subscription.unsubscribe().then((successful) => {
|
||||||
|
this.isPushEnabled = false;
|
||||||
|
}).catch((e) => {
|
||||||
|
this.log.error('Unsubscription error: ', e);
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
this.log.error('Error thrown while unsubscribing from ' +
|
||||||
|
'push messaging.', e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public forceUnsubscribe() {
|
||||||
|
if(!this.isAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.serviceWorker.ready.then((reg) => {
|
||||||
|
reg.pushManager.getSubscription().then((subscription) => {
|
||||||
|
this.log.warn('force unsubscribe', subscription);
|
||||||
|
if(subscription) {
|
||||||
|
subscription.unsubscribe().then((successful) => {
|
||||||
|
this.log.warn('force unsubscribe successful', successful);
|
||||||
|
this.isPushEnabled = false;
|
||||||
|
}).catch((e) => {
|
||||||
|
this.log.error('Unsubscription error: ', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
this.log.error('Error thrown while unsubscribing from ' +
|
||||||
|
'push messaging.', e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAliveNotify = () => {
|
||||||
|
if(!this.isAvailable || rootScope.idle && rootScope.idle.deactivated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settings.baseUrl = (location.href || '').replace(/#.*$/, '') + '#/im';
|
||||||
|
|
||||||
|
const lang: ServiceWorkerPingTask['payload']['lang'] = {} as any;
|
||||||
|
const ACTIONS_LANG_MAP: Record<keyof ServiceWorkerPingTask['payload']['lang'], LangPackKey> = {
|
||||||
|
push_action_mute1d: isMobile ? 'PushNotification.Action.Mute1d.Mobile' : 'PushNotification.Action.Mute1d',
|
||||||
|
push_action_settings: isMobile ? 'PushNotification.Action.Settings.Mobile' : 'PushNotification.Action.Settings',
|
||||||
|
push_message_nopreview: 'PushNotification.Message.NoPreview'
|
||||||
|
};
|
||||||
|
|
||||||
|
for(const action in ACTIONS_LANG_MAP) {
|
||||||
|
lang[action as keyof typeof ACTIONS_LANG_MAP] = I18n.format(ACTIONS_LANG_MAP[action as keyof typeof ACTIONS_LANG_MAP], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const task: ServiceWorkerPingTask = {
|
||||||
|
type: 'ping',
|
||||||
|
payload: {
|
||||||
|
localNotifications: this.localNotificationsAvailable,
|
||||||
|
lang: lang,
|
||||||
|
settings: this.settings
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(navigator.serviceWorker.controller) {
|
||||||
|
navigator.serviceWorker.controller.postMessage(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isAliveTO = setTimeout(this.isAliveNotify, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSettings(newSettings: WebPushApiManager['settings']) {
|
||||||
|
this.settings = copy(newSettings);
|
||||||
|
clearTimeout(this.isAliveTO);
|
||||||
|
this.isAliveNotify();
|
||||||
|
}
|
||||||
|
|
||||||
|
public hidePushNotifications() {
|
||||||
|
if(!this.isAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(navigator.serviceWorker.controller) {
|
||||||
|
const task: ServiceWorkerNotificationsClearTask = {type: 'notifications_clear'};
|
||||||
|
navigator.serviceWorker.controller.postMessage(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setUpServiceWorkerChannel() {
|
||||||
|
if(!this.isAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
apiManager.addServiceWorkerTaskListener('push_click', (task: ServiceWorkerPushClickTask) => {
|
||||||
|
if(rootScope.idle && rootScope.idle.deactivated) {
|
||||||
|
// AppRuntimeManager.reload(); // WARNING
|
||||||
|
location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rootScope.dispatchEvent('push_notification_click', task.payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.serviceWorker.ready.then(this.isAliveNotify);
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushSubscriptionNotify(event: PushSubscriptionNotifyType, subscription?: PushSubscription) {
|
||||||
|
if(subscription) {
|
||||||
|
const subscriptionObj: PushSubscriptionJSON = subscription.toJSON();
|
||||||
|
if(!subscriptionObj ||
|
||||||
|
!subscriptionObj.endpoint ||
|
||||||
|
!subscriptionObj.keys ||
|
||||||
|
!subscriptionObj.keys.p256dh ||
|
||||||
|
!subscriptionObj.keys.auth) {
|
||||||
|
this.log.warn('Invalid push subscription', subscriptionObj);
|
||||||
|
this.unsubscribe();
|
||||||
|
this.isAvailable = false;
|
||||||
|
this.pushSubscriptionNotify(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.warn('Push', event, subscriptionObj);
|
||||||
|
rootScope.dispatchEvent(('push_' + event) as PushSubscriptionNotifyEvent, {
|
||||||
|
tokenType: 10,
|
||||||
|
tokenValue: JSON.stringify(subscriptionObj)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.log.warn('Push', event, false);
|
||||||
|
rootScope.dispatchEvent(('push_' + event) as PushSubscriptionNotifyEvent, false as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const webPushApiManager = new WebPushApiManager();
|
||||||
|
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.webPushApiManager = webPushApiManager);
|
||||||
|
export default webPushApiManager;
|
@ -15,6 +15,8 @@ import type Chat from "../components/chat/chat";
|
|||||||
import type { UserAuth } from "./mtproto/mtproto_config";
|
import type { UserAuth } from "./mtproto/mtproto_config";
|
||||||
import type { State, Theme } from "./appManagers/appStateManager";
|
import type { State, Theme } from "./appManagers/appStateManager";
|
||||||
import type { MyDraftMessage } from "./appManagers/appDraftsManager";
|
import type { MyDraftMessage } from "./appManagers/appDraftsManager";
|
||||||
|
import type { PushSubscriptionNotify } from "./mtproto/webPushApiManager";
|
||||||
|
import type { PushNotificationObject } from "./serviceWorker/push";
|
||||||
import EventListenerBase from "../helpers/eventListenerBase";
|
import EventListenerBase from "../helpers/eventListenerBase";
|
||||||
import { MOUNT_CLASS_TO } from "../config/debug";
|
import { MOUNT_CLASS_TO } from "../config/debug";
|
||||||
|
|
||||||
@ -113,7 +115,13 @@ export type BroadcastEvents = {
|
|||||||
|
|
||||||
'theme_change': void,
|
'theme_change': void,
|
||||||
|
|
||||||
'instance_deactivated': void
|
'instance_activated': void,
|
||||||
|
'instance_deactivated': void,
|
||||||
|
|
||||||
|
'push_notification_click': PushNotificationObject,
|
||||||
|
'push_init': PushSubscriptionNotify,
|
||||||
|
'push_subscribe': PushSubscriptionNotify,
|
||||||
|
'push_unsubscribe': PushSubscriptionNotify,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class RootScope extends EventListenerBase<{
|
export class RootScope extends EventListenerBase<{
|
||||||
|
39
src/lib/serviceWorker/cache.ts
Normal file
39
src/lib/serviceWorker/cache.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ctx = self as any as ServiceWorkerGlobalScope;
|
||||||
|
export const CACHE_ASSETS_NAME = 'cachedAssets';
|
||||||
|
|
||||||
|
function isCorrectResponse(response: Response) {
|
||||||
|
return response.ok && response.status === 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestCache(event: FetchEvent) {
|
||||||
|
try {
|
||||||
|
const cache = await ctx.caches.open(CACHE_ASSETS_NAME);
|
||||||
|
const file = await cache.match(event.request, {ignoreVary: true});
|
||||||
|
|
||||||
|
if(file && isCorrectResponse(file)) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: HeadersInit = {'Vary': '*'};
|
||||||
|
let response = await fetch(event.request, {headers});
|
||||||
|
if(isCorrectResponse(response)) {
|
||||||
|
cache.put(event.request, response.clone());
|
||||||
|
} else if(response.status === 304) { // possible fix for 304 in Safari
|
||||||
|
const url = event.request.url.replace(/\?.+$/, '') + '?' + (Math.random() * 100000 | 0);
|
||||||
|
response = await fetch(url, {headers});
|
||||||
|
if(isCorrectResponse(response)) {
|
||||||
|
cache.put(event.request, response.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch(err) {
|
||||||
|
return fetch(event.request);
|
||||||
|
}
|
||||||
|
}
|
151
src/lib/serviceWorker/index.service.ts
Normal file
151
src/lib/serviceWorker/index.service.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// #if MTPROTO_SW
|
||||||
|
import '../mtproto/mtproto.worker';
|
||||||
|
/// #endif
|
||||||
|
//import CacheStorageController from '../cacheStorage';
|
||||||
|
import type { WorkerTaskTemplate, WorkerTaskVoidTemplate } from '../../types';
|
||||||
|
import type { InputFileLocation, UploadFile } from '../../layer';
|
||||||
|
import type { WebPushApiManager } from '../mtproto/webPushApiManager';
|
||||||
|
import { logger, LogTypes } from '../logger';
|
||||||
|
import { CancellablePromise } from '../../helpers/cancellablePromise';
|
||||||
|
import './push';
|
||||||
|
import { CACHE_ASSETS_NAME, requestCache } from './cache';
|
||||||
|
import onStreamFetch from './stream';
|
||||||
|
import { closeAllNotifications, onPing, PushNotificationObject } from './push';
|
||||||
|
|
||||||
|
export const log = logger('SW', LogTypes.Error | LogTypes.Debug | LogTypes.Log | LogTypes.Warn);
|
||||||
|
const ctx = self as any as ServiceWorkerGlobalScope;
|
||||||
|
export const deferredPromises: {[taskId: number]: CancellablePromise<any>} = {};
|
||||||
|
|
||||||
|
export interface RequestFilePartTask extends WorkerTaskTemplate {
|
||||||
|
type: 'requestFilePart',
|
||||||
|
payload: [number, InputFileLocation, number, number]
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RequestFilePartTaskResponse extends WorkerTaskTemplate {
|
||||||
|
type: 'requestFilePart',
|
||||||
|
payload?: UploadFile.uploadFile,
|
||||||
|
originalPayload?: RequestFilePartTask['payload']
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ServiceWorkerPingTask extends WorkerTaskVoidTemplate {
|
||||||
|
type: 'ping',
|
||||||
|
payload: {
|
||||||
|
localNotifications: boolean,
|
||||||
|
lang: {
|
||||||
|
push_action_mute1d: string
|
||||||
|
push_action_settings: string
|
||||||
|
push_message_nopreview: string
|
||||||
|
},
|
||||||
|
settings: WebPushApiManager['settings']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ServiceWorkerNotificationsClearTask extends WorkerTaskVoidTemplate {
|
||||||
|
type: 'notifications_clear'
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ServiceWorkerPushClickTask extends WorkerTaskVoidTemplate {
|
||||||
|
type: 'push_click',
|
||||||
|
payload: PushNotificationObject
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceWorkerTask = RequestFilePartTaskResponse | ServiceWorkerPingTask | ServiceWorkerNotificationsClearTask;
|
||||||
|
|
||||||
|
/// #if !MTPROTO_SW
|
||||||
|
const taskListeners: {
|
||||||
|
[type in ServiceWorkerTask['type']]: (task: any, event: ExtendableMessageEvent) => void
|
||||||
|
} = {
|
||||||
|
notifications_clear: () => {
|
||||||
|
closeAllNotifications();
|
||||||
|
},
|
||||||
|
ping: (task: ServiceWorkerPingTask, event) => {
|
||||||
|
onPing(task, event);
|
||||||
|
},
|
||||||
|
requestFilePart: (task: RequestFilePartTaskResponse) => {
|
||||||
|
const promise = deferredPromises[task.id];
|
||||||
|
|
||||||
|
if(task.error) {
|
||||||
|
promise.reject(task.error);
|
||||||
|
} else {
|
||||||
|
promise.resolve(task.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete deferredPromises[task.id];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ctx.addEventListener('message', (e) => {
|
||||||
|
const task = e.data as ServiceWorkerTask;
|
||||||
|
const callback = taskListeners[task.type];
|
||||||
|
if(callback) {
|
||||||
|
callback(task, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/// #endif
|
||||||
|
|
||||||
|
//const cacheStorage = new CacheStorageController('cachedAssets');
|
||||||
|
let taskId = 0;
|
||||||
|
|
||||||
|
export function getTaskId() {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementTaskId() {
|
||||||
|
return taskId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFetch = (event: FetchEvent): void => {
|
||||||
|
if(event.request.url.indexOf(location.origin + '/') === 0 && event.request.url.match(/\.(js|css|jpe?g|json|wasm|png|mp3|svg|tgs|ico|woff2?|ttf|webmanifest?)(?:\?.*)?$/)) {
|
||||||
|
return event.respondWith(requestCache(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || [];
|
||||||
|
|
||||||
|
//log.debug('[fetch]:', event);
|
||||||
|
|
||||||
|
switch(scope) {
|
||||||
|
case 'stream': {
|
||||||
|
onStreamFetch(event, params);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
event.respondWith(new Response('', {
|
||||||
|
status: 500,
|
||||||
|
statusText: 'Internal Server Error',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeState = () => {
|
||||||
|
ctx.onfetch = onFetch;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.addEventListener('install', (event) => {
|
||||||
|
log('installing');
|
||||||
|
event.waitUntil(ctx.skipWaiting()); // Activate worker immediately
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.addEventListener('activate', (event) => {
|
||||||
|
log('activating', ctx);
|
||||||
|
event.waitUntil(ctx.caches.delete(CACHE_ASSETS_NAME));
|
||||||
|
event.waitUntil(ctx.clients.claim());
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.onerror = (error) => {
|
||||||
|
log.error('error:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.onunhandledrejection = (error) => {
|
||||||
|
log.error('onunhandledrejection:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.onoffline = ctx.ononline = onChangeState;
|
||||||
|
|
||||||
|
onChangeState();
|
338
src/lib/serviceWorker/push.ts
Normal file
338
src/lib/serviceWorker/push.ts
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
/*
|
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*
|
||||||
|
* Originally from:
|
||||||
|
* https://github.com/zhukov/webogram
|
||||||
|
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
|
||||||
|
* https://github.com/zhukov/webogram/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Database } from "../../config/databases";
|
||||||
|
import DATABASE_STATE from "../../config/databases/state";
|
||||||
|
import { isFirefox } from "../../helpers/userAgent";
|
||||||
|
import IDBStorage from "../idb";
|
||||||
|
import { log, ServiceWorkerPingTask, ServiceWorkerPushClickTask } from "./index.service";
|
||||||
|
|
||||||
|
const ctx = self as any as ServiceWorkerGlobalScope;
|
||||||
|
const defaultBaseUrl = location.protocol + '//' + location.hostname + location.pathname.split('/').slice(0, -1).join('/') + '/';
|
||||||
|
|
||||||
|
export type PushNotificationObject = {
|
||||||
|
loc_key: string,
|
||||||
|
loc_args: string[],
|
||||||
|
//user_id: number, // should be number
|
||||||
|
custom: {
|
||||||
|
channel_id?: string, // should be number
|
||||||
|
chat_id?: string, // should be number
|
||||||
|
from_id?: string, // should be number
|
||||||
|
msg_id: string,
|
||||||
|
peerId?: string // should be number
|
||||||
|
},
|
||||||
|
sound?: string,
|
||||||
|
random_id: number,
|
||||||
|
badge?: string, // should be number
|
||||||
|
description: string,
|
||||||
|
mute: string, // should be number
|
||||||
|
title: string,
|
||||||
|
|
||||||
|
action?: 'mute1d' | 'push_settings', // will be set before postMessage to main thread
|
||||||
|
};
|
||||||
|
|
||||||
|
class SomethingGetter<T extends Database<any>, Storage extends Record<string, any>> {
|
||||||
|
private cache: Partial<Storage> = {};
|
||||||
|
private storage: IDBStorage<T>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
db: T,
|
||||||
|
storeName: typeof db['stores'][number]['name'],
|
||||||
|
private defaults: {
|
||||||
|
[Property in keyof Storage]: ((value: Storage[Property]) => Storage[Property]) | Storage[Property]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
this.storage = new IDBStorage<T>(db, storeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get<T extends keyof Storage>(key: T) {
|
||||||
|
if(this.cache[key] !== undefined) {
|
||||||
|
return this.cache[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: Storage[T];
|
||||||
|
try {
|
||||||
|
value = await this.storage.get(key as string);
|
||||||
|
} catch(err) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.cache[key] !== undefined) {
|
||||||
|
return this.cache[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(value === undefined) {
|
||||||
|
const callback = this.defaults[key];
|
||||||
|
value = typeof(callback) === 'function' ? callback() : callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.cache[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async set<T extends keyof Storage>(key: T, value: Storage[T]) {
|
||||||
|
this.cache[key] = value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.storage.save(key as string, value);
|
||||||
|
} catch(err) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushStorage = {
|
||||||
|
push_mute_until: number,
|
||||||
|
push_last_alive: number,
|
||||||
|
push_lang: any
|
||||||
|
push_settings: any
|
||||||
|
};
|
||||||
|
|
||||||
|
const getter = new SomethingGetter<typeof DATABASE_STATE, PushStorage>(DATABASE_STATE, 'session', {
|
||||||
|
push_mute_until: 0,
|
||||||
|
push_last_alive: 0,
|
||||||
|
push_lang: {},
|
||||||
|
push_settings: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.addEventListener('push', (event) => {
|
||||||
|
const obj: PushNotificationObject = event.data.json();
|
||||||
|
log('push', obj);
|
||||||
|
|
||||||
|
let hasActiveWindows = false;
|
||||||
|
const checksPromise = Promise.all([
|
||||||
|
getter.get('push_mute_until'),
|
||||||
|
getter.get('push_last_alive'),
|
||||||
|
ctx.clients.matchAll({type: 'window'})
|
||||||
|
]).then((result) => {
|
||||||
|
const [muteUntil, lastAliveTime, clientList] = result;
|
||||||
|
|
||||||
|
log('matched clients', clientList);
|
||||||
|
hasActiveWindows = clientList.length > 0;
|
||||||
|
if(hasActiveWindows) {
|
||||||
|
throw 'Supress notification because some instance is alive';
|
||||||
|
}
|
||||||
|
|
||||||
|
const nowTime = Date.now();
|
||||||
|
if(userInvisibleIsSupported() &&
|
||||||
|
muteUntil &&
|
||||||
|
nowTime < muteUntil) {
|
||||||
|
throw `Supress notification because mute for ${Math.ceil((muteUntil - nowTime) / 60000)} min`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!obj.badge) {
|
||||||
|
throw 'No badge?';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
checksPromise.catch(reason => {
|
||||||
|
log(reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
const notificationPromise = checksPromise.then(() => {
|
||||||
|
return Promise.all([getter.get('push_settings'), getter.get('push_lang')])
|
||||||
|
}).then((result) => {
|
||||||
|
return fireNotification(obj, result[0], result[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const closePromise = notificationPromise.catch(() => {
|
||||||
|
log('Closing all notifications on push', hasActiveWindows);
|
||||||
|
if(userInvisibleIsSupported() || hasActiveWindows) {
|
||||||
|
return closeAllNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.registration.showNotification('Telegram', {
|
||||||
|
tag: 'unknown_peer'
|
||||||
|
}).then(() => {
|
||||||
|
if(hasActiveWindows) {
|
||||||
|
return closeAllNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => closeAllNotifications(), hasActiveWindows ? 0 : 100);
|
||||||
|
}).catch((error) => {
|
||||||
|
log.error('Show notification error', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
event.waitUntil(closePromise);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.addEventListener('notificationclick', (event) => {
|
||||||
|
const notification = event.notification;
|
||||||
|
log('On notification click: ', notification.tag);
|
||||||
|
notification.close();
|
||||||
|
|
||||||
|
const action = event.action as PushNotificationObject['action'];
|
||||||
|
if(action === 'mute1d' && userInvisibleIsSupported()) {
|
||||||
|
log('[SW] mute for 1d');
|
||||||
|
getter.set('push_mute_until', Date.now() + 86400e3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: PushNotificationObject = notification.data;
|
||||||
|
if(!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = ctx.clients.matchAll({
|
||||||
|
type: 'window'
|
||||||
|
}).then((clientList) => {
|
||||||
|
data.action = action;
|
||||||
|
pendingNotification = {type: 'push_click', payload: data};
|
||||||
|
for(let i = 0; i < clientList.length; i++) {
|
||||||
|
const client = clientList[i];
|
||||||
|
if('focus' in client) {
|
||||||
|
client.focus();
|
||||||
|
client.postMessage(pendingNotification);
|
||||||
|
pendingNotification = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ctx.clients.openWindow) {
|
||||||
|
return getter.get('push_settings').then((settings) => {
|
||||||
|
return ctx.clients.openWindow(settings.baseUrl || defaultBaseUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
log.error('Clients.matchAll error', error);
|
||||||
|
})
|
||||||
|
|
||||||
|
event.waitUntil(promise);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.addEventListener('notificationclose', onCloseNotification);
|
||||||
|
|
||||||
|
let notifications: Set<Notification> = new Set();
|
||||||
|
let pendingNotification: ServiceWorkerPushClickTask;
|
||||||
|
function pushToNotifications(notification: Notification) {
|
||||||
|
if(!notifications.has(notification)) {
|
||||||
|
notifications.add(notification);
|
||||||
|
// @ts-ignore
|
||||||
|
notification.onclose = onCloseNotification;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCloseNotification(event: NotificationEvent) {
|
||||||
|
removeFromNotifications(event.notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromNotifications(notification: Notification) {
|
||||||
|
notifications.delete(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeAllNotifications() {
|
||||||
|
for(const notification of notifications) {
|
||||||
|
try {
|
||||||
|
notification.close();
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let promise: Promise<void>;
|
||||||
|
if('getNotifications' in ctx.registration) {
|
||||||
|
promise = ctx.registration.getNotifications({}).then((notifications) => {
|
||||||
|
for(let i = 0, len = notifications.length; i < len; ++i) {
|
||||||
|
try {
|
||||||
|
notifications[i].close();
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
log.error('Offline register SW error', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
promise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.clear();
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function userInvisibleIsSupported() {
|
||||||
|
return isFirefox;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fireNotification(obj: PushNotificationObject, settings: PushStorage['push_settings'], lang: PushStorage['push_lang']) {
|
||||||
|
const icon = 'assets/img/logo_filled_rounded.png';
|
||||||
|
let title = obj.title || 'Telegram';
|
||||||
|
let body = obj.description || '';
|
||||||
|
let peerId: number;
|
||||||
|
|
||||||
|
if(obj.custom) {
|
||||||
|
if(obj.custom.channel_id) {
|
||||||
|
peerId = -obj.custom.channel_id;
|
||||||
|
} else if(obj.custom.chat_id) {
|
||||||
|
peerId = -obj.custom.chat_id;
|
||||||
|
} else {
|
||||||
|
peerId = +obj.custom.from_id || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.custom.peerId = '' + peerId;
|
||||||
|
let tag = 'peer' + peerId;
|
||||||
|
|
||||||
|
if(settings && settings.nopreview) {
|
||||||
|
title = 'Telegram';
|
||||||
|
body = lang.push_message_nopreview || 'You have a new message';
|
||||||
|
tag = 'unknown_peer';
|
||||||
|
}
|
||||||
|
|
||||||
|
log('show notify', title, body, icon, obj);
|
||||||
|
|
||||||
|
const actions: (Omit<NotificationAction, 'action'> & {action: PushNotificationObject['action']})[] = [{
|
||||||
|
action: 'mute1d',
|
||||||
|
title: lang.push_action_mute1d || 'Mute for 24H'
|
||||||
|
}/* , {
|
||||||
|
action: 'push_settings',
|
||||||
|
title: lang.push_action_settings || 'Settings'
|
||||||
|
} */];
|
||||||
|
|
||||||
|
const notificationPromise = ctx.registration.showNotification(title, {
|
||||||
|
body,
|
||||||
|
icon,
|
||||||
|
tag,
|
||||||
|
data: obj,
|
||||||
|
actions
|
||||||
|
});
|
||||||
|
|
||||||
|
return notificationPromise.then((event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
if(event && event.notification) {
|
||||||
|
// @ts-ignore
|
||||||
|
pushToNotifications(event.notification);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
log.error('Show notification promise', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onPing(task: ServiceWorkerPingTask, event: ExtendableMessageEvent) {
|
||||||
|
const client = event.ports && event.ports[0] || event.source;
|
||||||
|
const payload = task.payload;
|
||||||
|
|
||||||
|
if(payload.localNotifications) {
|
||||||
|
getter.set('push_last_alive', Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pendingNotification &&
|
||||||
|
client &&
|
||||||
|
'postMessage' in client) {
|
||||||
|
client.postMessage(pendingNotification, []);
|
||||||
|
pendingNotification = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(payload.lang) {
|
||||||
|
getter.set('push_lang', payload.lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(payload.settings) {
|
||||||
|
getter.set('push_settings', payload.settings);
|
||||||
|
}
|
||||||
|
}
|
130
src/lib/serviceWorker/stream.ts
Normal file
130
src/lib/serviceWorker/stream.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { deferredPromise } from "../../helpers/cancellablePromise";
|
||||||
|
import { notifySomeone } from "../../helpers/context";
|
||||||
|
import { isSafari } from "../../helpers/userAgent";
|
||||||
|
import { UploadFile } from "../../layer";
|
||||||
|
import { DownloadOptions } from "../mtproto/apiFileManager";
|
||||||
|
import { RequestFilePartTask, deferredPromises, incrementTaskId } from "./index.service";
|
||||||
|
import timeout from "./timeout";
|
||||||
|
|
||||||
|
export default function onStreamFetch(event: FetchEvent, params: string) {
|
||||||
|
const range = parseRange(event.request.headers.get('Range'));
|
||||||
|
let [offset, end] = range;
|
||||||
|
|
||||||
|
const info: DownloadOptions = JSON.parse(decodeURIComponent(params));
|
||||||
|
//const fileName = getFileNameByLocation(info.location);
|
||||||
|
|
||||||
|
// ! если грузить очень большое видео чанками по 512Кб в мобильном Safari, то стрим не запустится
|
||||||
|
const limitPart = info.size > (75 * 1024 * 1024) ? STREAM_CHUNK_UPPER_LIMIT : STREAM_CHUNK_MIDDLE_LIMIT;
|
||||||
|
|
||||||
|
/* if(info.size > limitPart && isSafari && offset === limitPart) {
|
||||||
|
//end = info.size - 1;
|
||||||
|
//offset = info.size - 1 - limitPart;
|
||||||
|
offset = info.size - (info.size % limitPart);
|
||||||
|
} */
|
||||||
|
|
||||||
|
//log.debug('[stream]', url, offset, end);
|
||||||
|
|
||||||
|
event.respondWith(Promise.race([
|
||||||
|
timeout(45 * 1000),
|
||||||
|
|
||||||
|
new Promise<Response>((resolve, reject) => {
|
||||||
|
// safari workaround
|
||||||
|
const possibleResponse = responseForSafariFirstRange(range, info.mimeType, info.size);
|
||||||
|
if(possibleResponse) {
|
||||||
|
return resolve(possibleResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = end && end < limitPart ? alignLimit(end - offset + 1) : limitPart;
|
||||||
|
const alignedOffset = alignOffset(offset, limit);
|
||||||
|
|
||||||
|
//log.debug('[stream] requestFilePart:', /* info.dcId, info.location, */ alignedOffset, limit);
|
||||||
|
|
||||||
|
const task: RequestFilePartTask = {
|
||||||
|
type: 'requestFilePart',
|
||||||
|
id: incrementTaskId(),
|
||||||
|
payload: [info.dcId, info.location, alignedOffset, limit]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const deferred = deferredPromises[task.id] = deferredPromise<UploadFile.uploadFile>();
|
||||||
|
deferred.then(result => {
|
||||||
|
let ab = result.bytes as Uint8Array;
|
||||||
|
|
||||||
|
//log.debug('[stream] requestFilePart result:', result);
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'Content-Range': `bytes ${alignedOffset}-${alignedOffset + ab.byteLength - 1}/${info.size || '*'}`,
|
||||||
|
'Content-Length': `${ab.byteLength}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if(info.mimeType) headers['Content-Type'] = info.mimeType;
|
||||||
|
|
||||||
|
if(isSafari) {
|
||||||
|
ab = ab.slice(offset - alignedOffset, end - alignedOffset + 1);
|
||||||
|
headers['Content-Range'] = `bytes ${offset}-${offset + ab.byteLength - 1}/${info.size || '*'}`;
|
||||||
|
headers['Content-Length'] = `${ab.byteLength}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulate slow connection
|
||||||
|
//setTimeout(() => {
|
||||||
|
resolve(new Response(ab, {
|
||||||
|
status: 206,
|
||||||
|
statusText: 'Partial Content',
|
||||||
|
headers,
|
||||||
|
}));
|
||||||
|
//}, 2.5e3);
|
||||||
|
}).catch(err => {});
|
||||||
|
|
||||||
|
notifySomeone(task);
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseForSafariFirstRange(range: [number, number], mimeType: string, size: number): Response {
|
||||||
|
if(range[0] === 0 && range[1] === 1) {
|
||||||
|
return new Response(new Uint8Array(2).buffer, {
|
||||||
|
status: 206,
|
||||||
|
statusText: 'Partial Content',
|
||||||
|
headers: {
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'Content-Range': `bytes 0-1/${size || '*'}`,
|
||||||
|
'Content-Length': '2',
|
||||||
|
'Content-Type': mimeType || 'video/mp4',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* const STREAM_CHUNK_UPPER_LIMIT = 256 * 1024;
|
||||||
|
const SMALLEST_CHUNK_LIMIT = 256 * 4; */
|
||||||
|
/* const STREAM_CHUNK_UPPER_LIMIT = 1024 * 1024;
|
||||||
|
const SMALLEST_CHUNK_LIMIT = 1024 * 4; */
|
||||||
|
const STREAM_CHUNK_MIDDLE_LIMIT = 512 * 1024;
|
||||||
|
const STREAM_CHUNK_UPPER_LIMIT = 1024 * 1024;
|
||||||
|
const SMALLEST_CHUNK_LIMIT = 512 * 4;
|
||||||
|
|
||||||
|
function parseRange(header: string): [number, number] {
|
||||||
|
if(!header) return [0, 0];
|
||||||
|
const [, chunks] = header.split('=');
|
||||||
|
const ranges = chunks.split(', ');
|
||||||
|
const [offset, end] = ranges[0].split('-');
|
||||||
|
|
||||||
|
return [+offset, +end || 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignOffset(offset: number, base = SMALLEST_CHUNK_LIMIT) {
|
||||||
|
return offset - (offset % base);
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignLimit(limit: number) {
|
||||||
|
return 2 ** Math.ceil(Math.log(limit) / Math.log(2));
|
||||||
|
}
|
10
src/lib/serviceWorker/timeout.ts
Normal file
10
src/lib/serviceWorker/timeout.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export default function timeout(delay: number): Promise<Response> {
|
||||||
|
return new Promise(((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(new Response('', {
|
||||||
|
status: 408,
|
||||||
|
statusText: 'Request timed out.',
|
||||||
|
}));
|
||||||
|
}, delay);
|
||||||
|
}));
|
||||||
|
}
|
2
src/types.d.ts
vendored
2
src/types.d.ts
vendored
@ -29,6 +29,8 @@ export type WorkerTaskTemplate = {
|
|||||||
error?: ApiError
|
error?: ApiError
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkerTaskVoidTemplate = Omit<WorkerTaskTemplate, 'id'>;
|
||||||
|
|
||||||
export type Modify<T, R> = Omit<T, keyof R> & R;
|
export type Modify<T, R> = Omit<T, keyof R> & R;
|
||||||
|
|
||||||
//export type Parameters<T> = T extends (... args: infer T) => any ? T : never;
|
//export type Parameters<T> = T extends (... args: infer T) => any ? T : never;
|
||||||
|
Loading…
Reference in New Issue
Block a user