Browse Source

Web Push notifications supported

Closes #981
master
Igor Zhukov 8 years ago
parent
commit
11ccf1aa16
  1. 18
      app/js/controllers.js
  2. 7
      app/js/lib/mtproto.js
  3. 73
      app/js/lib/ng_utils.js
  4. 312
      app/js/lib/push_worker.js
  5. 2
      app/js/locales/en-us.json
  6. 2
      app/js/messages_manager.js
  7. 70
      app/js/services.js
  8. 7
      app/partials/desktop/settings_modal.html
  9. 7
      app/partials/mobile/settings_modal.html
  10. 4
      app/service_worker.js

18
app/js/controllers.js

@ -3989,7 +3989,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) @@ -3989,7 +3989,7 @@ angular.module('myApp.controllers', ['myApp.i18n'])
}
})
.controller('SettingsModalController', function ($rootScope, $scope, $timeout, $modal, AppUsersManager, AppChatsManager, AppPhotosManager, MtpApiManager, Storage, NotificationsManager, MtpApiFileManager, PasswordManager, ApiUpdatesManager, ChangelogNotifyService, LayoutSwitchService, AppRuntimeManager, ErrorService, _) {
.controller('SettingsModalController', function ($rootScope, $scope, $timeout, $modal, AppUsersManager, AppChatsManager, AppPhotosManager, MtpApiManager, Storage, NotificationsManager, MtpApiFileManager, PasswordManager, ApiUpdatesManager, ChangelogNotifyService, LayoutSwitchService, WebPushApiManager, AppRuntimeManager, ErrorService, _) {
$scope.profile = {}
$scope.photo = {}
$scope.version = Config.App.version
@ -4142,10 +4142,13 @@ angular.module('myApp.controllers', ['myApp.i18n']) @@ -4142,10 +4142,13 @@ angular.module('myApp.controllers', ['myApp.i18n'])
})
}
Storage.get('notify_nodesktop', 'send_ctrlenter', 'notify_volume', 'notify_novibrate', 'notify_nopreview').then(function (settings) {
Storage.get('notify_nodesktop', 'send_ctrlenter', 'notify_volume', 'notify_novibrate', 'notify_nopreview', 'notify_nopush').then(function (settings) {
$scope.notify.desktop = !settings[0]
$scope.send.enter = settings[1] ? '' : '1'
$scope.notify.pushAvailable = WebPushApiManager.isAvailable
$scope.notify.push = !settings[5]
if (settings[2] !== false) {
$scope.notify.volume = settings[2] > 0 && settings[2] <= 1.0 ? settings[2] : 0
} else {
@ -4196,6 +4199,17 @@ angular.module('myApp.controllers', ['myApp.i18n']) @@ -4196,6 +4199,17 @@ angular.module('myApp.controllers', ['myApp.i18n'])
$rootScope.$broadcast('settings_changed')
}
$scope.togglePush = function () {
$scope.notify.push = !$scope.notify.push
if ($scope.notify.push) {
Storage.remove('notify_nopush')
} else {
Storage.set({notify_nopush: true})
}
$rootScope.$broadcast('settings_changed')
}
$scope.togglePreview = function () {
$scope.notify.preview = !$scope.notify.preview

7
app/js/lib/mtproto.js

@ -1468,6 +1468,11 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) @@ -1468,6 +1468,11 @@ angular.module('izhukov.mtproto', ['izhukov.utils'])
}
MtpNetworker.prototype.processMessage = function (message, messageID, sessionID) {
var msgidInt = parseInt(messageID.toString(10).substr(0, -10), 10)
if (msgidInt % 2) {
console.warn('[MT] Server even message id: ', messageID, message)
return
}
// console.log('process message', message, messageID, sessionID)
switch (message._) {
case 'msg_container':
@ -1513,7 +1518,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) @@ -1513,7 +1518,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils'])
case 'message':
if (this.lastServerMessages.indexOf(messageID) != -1) {
console.warn('[MT] Server same messageID: ', messageID)
// console.warn('[MT] Server same messageID: ', messageID)
this.ackMessage(messageID)
return
}

73
app/js/lib/ng_utils.js

@ -394,7 +394,7 @@ angular.module('izhukov.utils', []) @@ -394,7 +394,7 @@ angular.module('izhukov.utils', [])
request.onsuccess = function (event) {
finished = true
db = request.result
var db = request.result
db.onerror = function (error) {
storageIsAvailable = false
@ -1981,7 +1981,7 @@ angular.module('izhukov.utils', []) @@ -1981,7 +1981,7 @@ angular.module('izhukov.utils', [])
return timeParams
})
.service('WebPushApiManager', function ($timeout, $q, $rootScope) {
.service('WebPushApiManager', function ($window, $timeout, $q, $rootScope, AppRuntimeManager) {
var isAvailable = true
var isPushEnabled = false
@ -2002,6 +2002,7 @@ angular.module('izhukov.utils', []) @@ -2002,6 +2002,7 @@ angular.module('izhukov.utils', [])
if (!started) {
started = true
getSubscription()
setUpServiceWorkerChannel()
}
}
@ -2011,13 +2012,7 @@ angular.module('izhukov.utils', []) @@ -2011,13 +2012,7 @@ angular.module('izhukov.utils', [])
}
navigator.serviceWorker.ready.then(function(reg) {
reg.pushManager.getSubscription().then(function(subscription) {
if (!subscription) {
console.log('Not yet subscribed to Push')
subscribe()
return
}
isPushEnabled = true
isPushEnabled = subscription ? true : false
pushSubscriptionNotify('init', subscription)
})
.catch(function(err) {
@ -2052,11 +2047,11 @@ angular.module('izhukov.utils', []) @@ -2052,11 +2047,11 @@ angular.module('izhukov.utils', [])
}
navigator.serviceWorker.ready.then(function(reg) {
reg.pushManager.getSubscription().then(function (subscription) {
pushSubscriptionNotify('unsubscribe', subscription)
isPushEnabled = false
if (subscription) {
pushSubscriptionNotify('unsubscribe', subscription)
setTimeout(function() {
subscription.unsubscribe().then(function(successful) {
isPushEnabled = false
@ -2073,12 +2068,55 @@ angular.module('izhukov.utils', []) @@ -2073,12 +2068,55 @@ angular.module('izhukov.utils', [])
})
}
function pushSubscriptionNotify(event, subscription) {
console.warn(dT(), 'Push', event, subscription.toJSON())
$rootScope.$emit('push_' + event, {
tokenType: 10,
tokenValue: JSON.stringify(subscription.toJSON())
function isAliveNotify() {
if (!isAvailable ||
$rootScope.idle && $rootScope.idle.deactivated) {
return
}
var baseUrl = (location.href || '').replace(/#.*$/, '') + '#/im'
var eventData = {type: 'alive', baseUrl: baseUrl}
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage(eventData)
}
setTimeout(isAliveNotify, 10000)
}
function hidePushNotifications() {
if (!isAvailable) {
return
}
if (navigator.serviceWorker.controller) {
var eventData = {type: 'notifications_clear'}
navigator.serviceWorker.controller.postMessage(eventData)
}
}
function setUpServiceWorkerChannel() {
navigator.serviceWorker.addEventListener('message', function(event) {
if (event.data &&
event.data.type == 'push_click') {
if ($rootScope.idle && $rootScope.idle.deactivated) {
AppRuntimeManager.reload()
return
}
$rootScope.$emit('push_notification_click', event.data.data)
}
})
navigator.serviceWorker.ready.then(isAliveNotify)
}
function pushSubscriptionNotify(event, subscription) {
if (subscription) {
console.warn(dT(), 'Push', event, subscription.toJSON())
$rootScope.$emit('push_' + event, {
tokenType: 10,
tokenValue: JSON.stringify(subscription.toJSON())
})
} else {
console.warn(dT(), 'Push', event, false)
$rootScope.$emit('push_' + event, false)
}
}
return {
@ -2086,7 +2124,8 @@ angular.module('izhukov.utils', []) @@ -2086,7 +2124,8 @@ angular.module('izhukov.utils', [])
start: start,
isPushEnabled: isPushEnabled,
subscribe: subscribe,
unsubscribe: unsubscribe
unsubscribe: unsubscribe,
hidePushNotifications: hidePushNotifications
}
})

312
app/js/lib/push_worker.js

@ -1,47 +1,323 @@ @@ -1,47 +1,323 @@
console.log('Push worker placeholder')
'use strict';
console.log('[SW] Push worker started')
var port
var lastAliveTime = false
var pendingNotification = false
var muteUntil = false
var baseUrl
switch (location.hostname) {
case 'localhost':
baseUrl = 'http://localhost:8000/app/index.html#/im'
break
case 'zhukov.github.io':
baseUrl = 'https://zhukov.github.io/webogram/#/im'
break
default:
case 'web.telegram.org':
baseUrl = 'https://' + location.hostname + '/#/im'
}
self.addEventListener('push', function(event) {
var obj = event.data.json()
console.log('push obj', obj)
fireNotification(obj, event)
console.log('[SW] push', obj)
if (!obj.badge) {
closeAllNotifications(obj, event)
} else {
fireNotification(obj, event)
}
})
self.onmessage = function(e) {
console.log(e)
port = e.ports[0]
}
self.addEventListener('activate', function(event) {
event.waitUntil(clients.claim())
})
self.addEventListener('message', function(event) {
console.log('[SW] on message', event.data)
port = event.ports[0] || event.source
if (event.data.type == 'alive') {
lastAliveTime = +(new Date())
if (pendingNotification &&
port &&
'postMessage' in port) {
port.postMessage(pendingNotification)
pendingNotification = false
}
}
if (event.data.type == 'notifications_clear') {
closeAllNotifications(event.data, event)
}
if (event.data.baseUrl) {
baseUrl = event.data.baseUrl
}
})
function fireNotification(obj, event) {
var nowTime = +(new Date())
if (nowTime - lastAliveTime < 60000) {
console.log('Supress notification because some instance is alive')
return false
}
if (muteUntil && nowTime < muteUntil) {
console.log('Supress notification because mute for ', (muteUntil - nowTime) / 60000, 'min')
return false
}
var title = obj.title || 'Telegram'
var body = obj.description || ''
var icon = 'img/Telegram72.png'
event.waitUntil(self.registration.showNotification(title, {
var icon = 'img/logo_share.png'
var peerID
if (obj.custom && obj.custom.channel_id) {
peerID = -obj.custom.channel_id
}
else if (obj.custom && obj.custom.chat_id) {
peerID = -obj.custom.chat_id
}
else {
peerID = obj.custom && obj.custom.from_id || 0
}
obj.custom.peerID = peerID
var notificationPromise = self.registration.showNotification(title, {
body: body,
icon: icon
icon: icon,
tag: 'peer' + peerID,
data: obj,
actions: [
{
action: 'mute1d',
title: 'Mute background alerts for 1 day'
},
{
action: 'push_settings',
title: 'Background alerts settings'
}
]
})
var finalPromise = notificationPromise.then(function (event) {
if (event && event.notification) {
pushToNotifications(event.notification)
}
})
event.waitUntil(finalPromise)
return true
}
var notifications = []
function pushToNotifications(notification) {
if (notifications.indexOf(notification) == -1) {
notifications.push(notification)
notification.onclose = onCloseNotification
}
}
function onCloseNotification(event) {
muteUntil = Math.max(muteUntil || 0, +(new Date()) + 600000) // 10 min
removeFromNotifications(event.notification)
}
function removeFromNotifications(notification) {
console.warn('on close', notification)
var pos = notifications.indexOf(notification)
if (pos != -1) {
notifications.splice(pos, 1)
}
}
function closeAllNotifications(obj, event) {
for (var i = 0, len = notifications.length; i < len; i++) {
try {
notifications[i].close()
} catch (e) {}
}
event.waitUntil(self.registration.getNotifications({}).then(function(notifications) {
for (var i = 0, len = notifications.length; i < len; i++) {
try {
notifications[i].close()
} catch (e) {}
}
}))
notifications = []
}
self.addEventListener('notificationclick', function(event) {
console.log('On notification click: ', event.notification.tag)
event.notification.close()
var notification = event.notification
console.log('On notification click: ', notification.tag)
notification.close()
var action = event.action
if (action == 'mute1d') {
console.log('[SW] mute for 1d')
muteUntil = +(new Date()) + 86400000
IDBManager.setItem('mute_until', muteUntil.toString())
return
}
// This looks to see if the current is already open and
// focuses if it is
event.waitUntil(clients.matchAll({
type: 'window'
}).then(function(clientList) {
notification.data.action = action
pendingNotification = {type: 'push_click', data: notification.data}
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i]
if ('focus' in client) {
return client.focus()
client.focus()
;(port || client).postMessage(pendingNotification)
pendingNotification = false
return
}
}
if (clients.openWindow)
return clients.openWindow('')
if (clients.openWindow) {
return clients.openWindow(baseUrl)
}
}))
})
self.addEventListener('notificationclose', onCloseNotification)
;(function () {
var dbName = 'keyvalue'
var dbStoreName = 'kvItems'
var dbVersion = 2
var openDbPromise
var idbIsAvailable = self.indexedDB !== undefined &&
self.IDBTransaction !== undefined
function isAvailable () {
return idbIsAvailable
}
function openDatabase () {
if (openDbPromise) {
return openDbPromise
}
return openDbPromise = new Promise(function (resolve, reject) {
try {
var request = indexedDB.open(dbName, dbVersion)
var createObjectStore = function (db) {
db.createObjectStore(dbStoreName)
}
if (!request) {
throw new Exception()
}
} catch (error) {
console.error('error opening db', error.message)
idbIsAvailable = false
return $q.reject(error)
}
var finished = false
setTimeout(function () {
if (!finished) {
request.onerror({type: 'IDB_CREATE_TIMEOUT'})
}
}, 3000)
request.onsuccess = function (event) {
finished = true
var db = request.result
db.onerror = function (error) {
idbIsAvailable = false
console.error('Error creating/accessing IndexedDB database', error)
reject(error)
}
resolve(db)
}
request.onerror = function (event) {
finished = true
idbIsAvailable = false
console.error('Error creating/accessing IndexedDB database', event)
reject(event)
}
request.onupgradeneeded = function (event) {
finished = true
console.warn('performing idb upgrade from', event.oldVersion, 'to', event.newVersion)
var db = event.target.result
if (event.oldVersion == 1) {
db.deleteObjectStore(dbStoreName)
}
createObjectStore(db)
}
})
}
function setItem (key, value) {
return openDatabase().then(function (db) {
try {
var objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName)
var request = objectStore.put(value, key)
} catch (error) {
idbIsAvailable = false
return Promise.reject(error)
}
return new Promise(function(resolve, reject) {
request.onsuccess = function (event) {
resolve(value)
}
request.onerror = function (error) {
reject(error)
}
})
})
}
function getItem (key) {
return openDatabase().then(function (db) {
return new Promise(function(resolve, reject) {
var objectStore = db.transaction([dbStoreName], IDBTransaction.READ || 'readonly').objectStore(dbStoreName)
var request = objectStore.get(key)
request.onsuccess = function (event) {
var result = event.target.result
if (result === undefined) {
reject()
} else {
resolve(result)
}
}
request.onerror = function (error) {
reject(error)
}
})
})
}
openDatabase()
self.IDBManager = {
name: 'IndexedDB',
isAvailable: isAvailable,
setItem: setItem,
getItem: getItem
}
})()
IDBManager.getItem('mute_until').then(function (newMuteUntil) {
muteUntil = Math.max(muteUntil || 0, newMuteUntil || 0) || false
})

2
app/js/locales/en-us.json

@ -59,10 +59,12 @@ @@ -59,10 +59,12 @@
"settings_modal_active_sessions": "Active sessions",
"settings_modal_settings": "Settings",
"settings_modal_notification_alert": "Notification alerts",
"settings_modal_notification_push": "PUSH notifications",
"settings_modal_vibrate": "Vibrate",
"settings_modal_sounds": "Sounds",
"settings_modal_language": "Language",
"settings_modal_notifications": "Desktop notifications",
"settings_modal_pushes": "Background notifications",
"settings_modal_message_preview": "Message preview",
"settings_modal_sound": "Sound",
"settings_modal_enter_send_description_md": "**Enter** - send message, **Shift + Enter** - new line",

2
app/js/messages_manager.js

@ -260,6 +260,8 @@ angular.module('myApp.services') @@ -260,6 +260,8 @@ angular.module('myApp.services')
if (hasPrepend &&
!newDialogsHandlePromise) {
newDialogsHandlePromise = $timeout(handleNewDialogs, 0)
} else {
$rootScope.$broadcast('dialogs_multiupdate', {})
}
})
}

70
app/js/services.js

@ -2512,7 +2512,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) @@ -2512,7 +2512,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
}
})
.service('AppInlineBotsManager', function (qSync, $q, $rootScope, toaster, Storage, ErrorService, MtpApiManager, AppMessagesManager, AppMessagesIDsManager, AppDocsManager, AppPhotosManager, AppGamesManager, RichTextProcessor, AppUsersManager, AppPeersManager, PeersSelectService, GeoLocationManager) {
.service('AppInlineBotsManager', function (qSync, $q, $rootScope, toaster, Storage, ErrorService, MtpApiManager, AppMessagesManager, AppMessagesIDsManager, AppDocsManager, AppPhotosManager, AppGamesManager, RichTextProcessor, AppUsersManager, AppPeersManager, LocationParamsService, PeersSelectService, GeoLocationManager) {
var inlineResults = {}
return {
@ -3516,7 +3516,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) @@ -3516,7 +3516,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
}
})
.service('NotificationsManager', function ($rootScope, $window, $interval, $q, _, MtpApiManager, AppPeersManager, IdleManager, Storage, AppRuntimeManager, FileManager, WebPushApiManager) {
.service('NotificationsManager', function ($rootScope, $window, $interval, $q, $modal, _, MtpApiManager, AppPeersManager, AppChatsManager, AppUsersManager, IdleManager, Storage, AppRuntimeManager, FileManager, WebPushApiManager) {
navigator.vibrate = navigator.vibrate || navigator.mozVibrate || navigator.webkitVibrate
var notificationsMsSiteMode = false
@ -3598,9 +3598,17 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) @@ -3598,9 +3598,17 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
})
var registeredDevice = false
var pushInited = false
$rootScope.$on('push_init', function (e, tokenData) {
if (tokenData) {
registerDevice(tokenData)
pushInited = true
if (!settings.nodesktop && !settings.nopush) {
if (tokenData) {
registerDevice(tokenData)
} else {
WebPushApiManager.subscribe()
}
} else {
unregisterDevice(tokenData)
}
})
$rootScope.$on('push_subscribe', function (e, tokenData) {
@ -3610,6 +3618,42 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) @@ -3610,6 +3618,42 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
unregisterDevice(tokenData)
})
var topMessagesDeferred = $q.defer()
var unregisterTopMsgs = $rootScope.$on('dialogs_multiupdate', function () {
unregisterTopMsgs()
topMessagesDeferred.resolve()
})
var topMessagesPromise = topMessagesDeferred.promise
$rootScope.$on('push_notification_click', function (e, notificationData) {
if (notificationData.action == 'push_settings') {
$modal.open({
templateUrl: templateUrl('settings_modal'),
controller: 'SettingsModalController',
windowClass: 'settings_modal_window mobile_modal',
backdrop: 'single'
})
return
}
var peerID = notificationData.custom && notificationData.custom.peerID
console.log('click', notificationData, peerID)
if (peerID) {
topMessagesPromise.then(function () {
if (notificationData.custom.channel_id &&
!AppChatsManager.hasChat(notificationData.custom.channel_id)) {
return
}
if (peerID > 0 && !AppUsersManager.hasUser(peerID)) {
return
}
$rootScope.$broadcast('history_focus', {
peerString: AppPeersManager.getPeerString(peerID)
})
})
}
})
return {
start: start,
notify: notify,
@ -3627,7 +3671,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) @@ -3627,7 +3671,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
}
function updateNotifySettings () {
Storage.get('notify_nodesktop', 'notify_volume', 'notify_novibrate', 'notify_nopreview').then(function (updSettings) {
Storage.get('notify_nodesktop', 'notify_volume', 'notify_novibrate', 'notify_nopreview', 'notify_nopush').then(function (updSettings) {
settings.nodesktop = updSettings[0]
settings.volume = updSettings[1] === false
? 0.5
@ -3635,6 +3679,20 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) @@ -3635,6 +3679,20 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
settings.novibrate = updSettings[2]
settings.nopreview = updSettings[3]
settings.nopush = updSettings[4]
if (pushInited) {
var needPush = !settings.nopush && !settings.nodesktop && WebPushApiManager.isAvailable || false
var hasPush = registeredDevice !== false
if (needPush != hasPush) {
if (needPush) {
WebPushApiManager.subscribe()
} else {
WebPushApiManager.unsubscribe()
}
}
}
})
}
@ -3901,6 +3959,8 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) @@ -3901,6 +3959,8 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
}
notificationsShown = {}
notificationsCount = 0
WebPushApiManager.hidePushNotifications()
}
function registerDevice (tokenData) {

7
app/partials/desktop/settings_modal.html

@ -61,6 +61,13 @@ @@ -61,6 +61,13 @@
<span class="tg_checkbox_label" my-i18n="settings_modal_notifications"></span>
</a>
<a ng-if="notify.desktop && notify.pushAvailable" class="md_modal_section_toggle_wrap tg_checkbox" ng-click="togglePush()" ng-class="notify.push ? 'tg_checkbox_on' : ''">
<span class="icon icon-checkbox-outer"><i class="icon-checkbox-inner"></i></span>
<span class="tg_checkbox_label" my-i18n="settings_modal_pushes"></span>
</a>
<a class="md_modal_section_toggle_wrap tg_checkbox" ng-click="togglePreview()" ng-class="notify.preview ? 'tg_checkbox_on' : ''">
<span class="icon icon-checkbox-outer"><i class="icon-checkbox-inner"></i></span>
<span class="tg_checkbox_label" my-i18n="settings_modal_message_preview"></span>

7
app/partials/mobile/settings_modal.html

@ -86,6 +86,13 @@ @@ -86,6 +86,13 @@
</a>
</div>
<div ng-if="notify.desktop && notify.pushAvailable" class="mobile_modal_action_wrap">
<a class="mobile_modal_action tg_checkbox clearfix" ng-click="togglePush()" ng-class="notify.push ? 'tg_checkbox_on' : ''">
<span class="icon icon-checkbox-outer"><i class="icon-checkbox-inner"></i></span>
<span class="tg_checkbox_label" my-i18n="settings_modal_notification_push"></span>
</a>
</div>
<div class="mobile_modal_action_wrap">
<a class="mobile_modal_action tg_checkbox clearfix" ng-click="togglePreview()" ng-class="notify.preview ? 'tg_checkbox_on' : ''">
<span class="icon icon-checkbox-outer"><i class="icon-checkbox-inner"></i></span>

4
app/service_worker.js

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
// Version 7
importScripts('js/lib/push_worker.js?3')
// Version 44
importScripts('js/lib/push_worker.js?44')

Loading…
Cancel
Save