NodeBB PWA
-
Not one website I use is a PWA as far as I can tell.
-
@meetdilip said in NodeBB PWA:
Not a single free software was responsive when NodeBB was introduced as well
I think he's got you there @HolyPhoenix
-
I've worked on a plugin to get NodeBB working as a PWA with service workers and push notifications / caching successfully. You only need to edit src/controllers/index.js around line 303, for all things manifest.json. Which works fine to edit directly ( stashing your changes on a pull ) even through upgrades.
Making your own service worker, you just do by what google says to do. But through nginx you tell it to read the serviceworker.js separately like so:
location ~ /serviceworker.js
{
root /path/to/nodebb/public;
}
putting it into the directory with "ln -s" command worked out well for me so I can edit changes without having to go back into the nodebb dir.On the plugin client.js you put in all of your service worker registration inside:
$(window).on('load', function(){
if ('serviceWorker' in navigator) { .... }
}Other side note about service worker's is they use a function called fetch which is just a proper HTTP post/get/etc. which is annoying to deal with because nginx serves nodebb which then nodebb catches all posts&gets unless you hand configure each request through nginx.
So you can 'override' those fetch calls with socket.io to communicate with the service worker:
library.js: socketPlugins = require.main.require('./src/socket.io/plugins')
client.js: socket.emit('command blah blah', { data: json }, function(err, data){ ... })Youtube and many other sites have started using service workers which make them PWA's (only to the extent at which they choose to utilize the service worker abilities)
https://www.youtube.com/sw.js
https://www.theguardian.com/service-worker.js
https://www.linkedin.com/voyager/service-worker.js -
I've worked on a plugin to get NodeBB working as a PWA with service workers and push notifications / caching successfully.
It would be great to see what you have.
You only need to edit src/controllers/index.js around line 303, for all things manifest.json
We can add a new hook for this to make it easier. Something like
filter:manifest.build
location ~ /serviceworker.js { root /path/to/nodebb/public; }
Hmmm. It does look like
serviceworker.js
has to be at the root of the site, maybe something like registering a route for it and redirecting to the one in assets would be better:router.get('/serviceworker.js', function (req, res) { res.redirect('/plugins/nodebb-plugin-pwa/serviceworkers.js'); });
Then it gets automatically handled by our static file server or by nginx when it's configured to serve our static files.
putting it into the directory with "ln -s" command worked out well for me so I can edit changes without having to go back into the nodebb dir
If you make an actual plugin, putting
"staticDirs": { "": "public" }
in the plugin.json and then placing serviceworker.js in the public directory of your plugin folder will work with route handler code above.
Other side note about service worker's is they use a function called fetch which is just a proper HTTP post/get/etc. which is annoying to deal with because nginx serves nodebb which then nodebb catches all posts&gets unless you hand configure each request through nginx.
I don't see why this is an issue. What is it requesting?
fetch
is just an AJAX request, so registering routes in express to handle the requests should be just as easy as using sockets. That is, unless you need to push data to the client. But server push is made for that, so... -
@PitaJ said in NodeBB PWA:
That all sounds really groovy! Being able to implement all the changes directly in the plugin would be obviously the best way to go no doubt. I'm going to try and update what changes I can make as soon as I can.I am not entirely comfortable sharing the website to the world because it's for a close-nit group of veterans, barely getting them onto it right now. So I can share that privately with you.
I don't see why this is an issue. What is it requesting?
fetch
is just an AJAX request, so registering routes in express to handle the requests should be just as easy as using sockets. That is, unless you need to push data to the client. But server push is made for that, so...I would agree with you. I'm not entirely familiar enough with
fetch
to say one way or the other. I did not think to try and register routes with express, that could've been easier. Like you said it is just pushing data back and forth to the client and server. If I were to leave the fetch commands from sw examples I would have registered express routes, I'm more familiar with socket.io so I used those. But getting data to the service worker for a push notification uses the web-push library which sends the payload (for push notifications) to the push-server (firebase is an easy one to make) then onto the service worker. Getting dynamic data into the service worker has been a bit of a challenge to say the least.To more specifically answer what it is requesting is really dependent on what you want to do with the service worker. Mozilla's website is the best example I've found of all the ways to monetize service workers. My goal was to get push notifications so there's some exchanges between the client and service upon service worker registration to save subscription information or read subscription information so push notifications can work. Those go through the database obviously but both the client and the server need to know the subscription keys and which user has which keys.
-
I am not entirely comfortable sharing the website to the world
I was talking about the code you have and the modifications you made. Thanks
-
@PitaJ
My bad, your wish is my commandI tried to clean up the three files to just what's relevant about PWA. Besides having to have a plugin you may also need a theme plugin because a button needs to be in place to be act as a registration for web-push.
---- Library.js ---- "use strict"; // Dependencies var async = module.parent.require('async'), plugin = {}, user= require.main.require('./src/user'), groups_pwa = require.main.require('./src/groups'), socketPlugins = require.main.require('./src/socket.io/plugins'), messaging = require.main.require('./src/messaging'), db = require.main.require('./src/database'); try{ var webPush = module.parent.require('web-push'); // Use the web-push library to hide the implementation details of the communication between the application server and the push service. // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and https://tools.ietf.org/html/draft-ietf-webpush-encryption. webPush.setGCMAPIKey('API KEY FROM FIREBASE'); }catch(err){ console.log("{!} Web Push dependency is not installed, PUSHING WILL NOT WORK " + err); console.log("{!} install: npm install web-push --save "); } /** * Initialize the plugin's pages * -> All socket.io communication needs to be established here * -> Hijacking of SW fetch communication done here **/ plugin.init = function(data, callback) { var controllers = require('./controllers'); // We create two routes for every view. One API call, and the actual route itself. // Just add the buildHeader middleware to your route and NodeBB will take care of everything for you. data.router.get('/admin/plugins/pwa', data.middleware.admin.buildHeader, controllers.renderAdminPage); data.router.get('/api/admin/plugins/pwa', controllers.renderAdminPage); /** * Socket.io Comms * - Create a socket namespace **/ socketPlugins.pwa = {}; // OVERRRIDE: app.post(route + 'sendNotification', function(req, res) ... socketPlugins.pwa.sendNotification = function(socket, req, callback) { /** * notification_options = { endpoint: endpoint, key: key, authSecret: authSecret, payload: message, delay: pushDelay, ttl: pushTTL, url: url }; **/ console.log("[+] -sendNotification- received from client - req: ", req); // Send notification to the push service. // Remove the endpoint from the subscriptions array if the push service responds with an error. Subscription has been cancelled or expired. // SHould loop through Database of each FR //subscriptions.forEach( plugin.sendNotification ); plugin.sendTodataSetOfUsers(req, callback); callback(null, { received: true }); }; // OVERRIDE: app.post(route + 'register', function(req, res) ... socketPlugins.pwa.register = function(socket, req, callback) { console.log("[+] -register- received from client - req.body: ", req); // Save the subscription to the Database var subscription = { user: req.user, key: req.key, authSecret: req.authSecret, endpoint: req.endpoint }; plugin.saveSubscription(subscription, function(err){ if(err){ console.log("{!} Error saving subscription: ",err); return callback(err); } }); console.log('\n[+] Subscription registered: ' + req.user ); callback(null, { received: true }); }; // OVERRIDE: app.post(route + 'unregister', function(req, res) ... socketPlugins.pwa.unregister = function(socket, req, callback) { console.log("[+] -UNregister- received from client - req.body: ", req); // Unregister a subscription by removing it from the subscriptions array async.waterfall([ function(next){ plugin.isSubscribed( req.user ); }, function(boolean, next){ if( boolean ){ // subscriptions.splice(subscriptions.indexOf(endpoint), 1); plugin.removeSubscription(req.uid); console.log('\n[+] Subscription unregistered: ', req.user); } else{ console.log('\n[!] Cannot unregister, no subscription found for uid:', req.user); } } ], callback); callback(null, { received: true }); }; callback(); }; /** * Check Subscription * data == uid **/ plugin.isSubscribed = function(data, callback) { var retBoolean = false; async.waterfall([ function(next){ db.isObjectField("user:"+data, "subscription",next); }, function(exits, next){ if(exits) { async.waterfall([ function(next){ db.getObjectField("user:"+data, "subscription",next); }, function(boolean, next){ retBoolean = boolean; } ], callback); } } ], callback); return retBoolean; }; /** * Send Notifications to dataSetOfUsers * https://caolan.github.io/async/docs.html#waterfall * The scope for async functions are within where they are created, not executed. * https://stackoverflow.com/questions/27415400/lost-of-scope-with-async-waterfall **/ plugin.sendToUsers = function(data, callback) { dataSetOfUsers.forEach(function (uid){ console.log(" Checking for subscription user: ", uid); async.waterfall([ function(next){ db.isObjectField("user:"+uid, "subscription",next); }, function(exits, next){ if(exits) { async.waterfall([ function(next){ db.getObjectField("user:"+uid, "subscription",next); }, function(boolean, next){ // There is a subscription and it's set to true if(boolean) { console.log("Subscription Found! ",uid); var smallscope = { endpoint: '', key: '', authSecret: '', TTL: '', payload: '', uid: '', userslug: '' }; async.waterfall([ function(next){ db.getObjectField("user:"+uid, "endpoint", next); }, function(saveme, next){ smallscope.endpoint = saveme; db.getObjectField("user:"+uid, "authSecret", next); }, function(saveme, next){ smallscope.authSecret = saveme; db.getObjectField("user:"+uid, "key", next); }, function(saveme, next){ smallscope.key = saveme; db.getObjectField("user:"+uid,"userslug",next); }, function(saveme,next){ smallscope.userslug = saveme; smallscope.TTL = data.ttl; smallscope.payload = data.payload; // BOOKMARK TODO smallscope.uid = uid; var sendUrl = url; // BOOKMARK TODO sendUrl += '/user/'; sendUrl += smallscope.userslug; sendUrl += '/chats/'; sendUrl += chatID; plugin.sendNotification(smallscope,callback); }, function(err, result){ console.log("\n{!push} error: ", err); } ], next); } else{ console.log("{!push} Subscription is set to False for: ", uid); smallscope = {}; return; } } ], callback); } else{ console.log("{!push} No Subscription Data: ", uid); //sendTo = {}; return; } } ], callback); }); }; /** * Save subscription after registration **/ plugin.saveSubscription = function(data, callback) { // Async all the things, Database abstractions do already test for errors async.waterfall([ function(next){ db.setObjectField("user:"+data.user, "subscription", true, next); db.setObjectField("user:"+data.user,"key",data.key, next); db.setObjectField("user:"+data.user,"authSecret",data.authSecret, next); db.setObjectField("user:"+data.user,"endpoint",data.endpoint, next); } ], callback ); }; /** * Remove subscription by matching the endpoint **/ plugin.removeSubscription = function(data) { console.log("\n[*] Removing from DB: ", data); }; /** * Send Notification to endpoint * * https://serviceworke.rs/push-subscription-management.html * + * https://serviceworke.rs/push-payload.html * -> data.payload (a string or NodeJS buffer) is all that is allowed to be sent to the webPush API **/ plugin.sendNotification = function (data, callback) { webPush.sendNotification( { endpoint: data.endpoint, TTL: data.ttl, keys: { p256dh: data.key, auth: data.authSecret } }, data.payload ).then(function() { console.log('\n[web-push] Firbase given: ' + data.key, ' for uid: ', data.uid); }).catch(function(err) { console.log('\n{!} ERROR in sending Notification: ', err); console.log('\n{!} endpoint removed: ' + data.endpoint); plugin.removeSubscription(data.endpoint); }); }; // Register Plugin to NodeBB module.exports = plugin;
---- Client.js ---- /* globals $, navigator, socket, app */ "use strict"; var key; var authSecret; var endpoint; var debug = false; // from the initial pwa messages to be quiet var pushDelay = 500; // time in miliseconds to delay sending the push notification out (on the service worker) var pushTTL = 60 * 60 * 24; // is a value in seconds that determines retention by the push service (a.k.a firebase endpoint) to continue to send message $(document).ready(function() { // Not using because so many elements are loaded in the window that need to be used for subscribing SW }); /** * All elments inside the DOM, such as divs are loaded **/ $(window).on('load', function(){ // Subscription Object that returns a promise function getSubscription() { console.log("[1] If #2 doesn't follow - reset the entire page(s) cache/cookies/tab_close "); return navigator.serviceWorker.ready.then(function(registration){ console.log("[2] #2, whole service worker is cleanly loaded! "); return registration.pushManager.getSubscription(); }); } /** * Service Worker Registration * https://developers.google.com/web/fundamentals/getting-started/primers/service-workers * -> adding Push subscription **/ // Register service worker and check the initial subscription state. if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(function(registration) { // Registration was successful console.log('[ServiceWorker] registration successful with scope: ', registration.scope); getSubscription().then(function(subscription) { if (subscription) { console.log('[!] Notifications: Already subscribed', subscription.endpoint); } else { console.log('[!] Notifications: User is not subscribed! '); } }); }).catch(function(err) { // registration failed :( console.log('{!} ERROR: ServiceWorker registration failed: ', err); }); } // END if('serviceWorker' in navigator) /** * Subscription For web Push Notifications * Push Subscription; Button comes from templates/partials/subscribe.tpl * https://serviceworke.rs/push-subscription-management_index_doc.html **/ //Get the registration from service worker and create a new subscription using registration.pushManager.subscribe. Then register received new subscription by sending a POST request with its endpoint to the server. function subscribe() { navigator.serviceWorker.ready.then(function(registration) { return registration.pushManager.subscribe({ userVisibleOnly: true }); }).then(function(subscription) { console.log('\n[*] Subscribed ', subscription.endpoint); // Retrieve the userโs public key. var rawKey = subscription.getKey ? subscription.getKey('p256dh') : ''; key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : ''; var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : ''; authSecret = rawAuthSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : ''; endpoint = subscription.endpoint; var subscription_object = { endpoint: endpoint, key: key, authSecret: authSecret, user: app.user.uid }; // fetch('register') Override return socket.emit('plugins.pwa.register', subscription_object, function(err, data) { if (err) { // BIG USER FAIL ERROR! MORE CLOSER ATTENTION FOR HANDELING! console.log("[!] plugins.pwa.register ", err); return app.alertError(err.message); } console.log("[+] plugins.pwa.register received: ", data.received); }); // Original register code // return fetch('register', { // method: 'post', // headers: { // 'Content-type': 'application/json' // }, // body: JSON.stringify({ // endpoint: subscription.endpoint, // key: key, // authSecret: authSecret, // }), // }); }).then(setUnsubscribeButton); } // Get existing subscription from service worker, unsubscribe (subscription.unsubscribe()) and unregister it in the server with a POST request to stop sending push messages to unexisting endpoint. function unsubscribe() { getSubscription().then(function(subscription) { return subscription.unsubscribe().then(function() { console.log('\n[!] Unsubscribed ', subscription.endpoint); var subscription_object = { endpoint: subscription.endpoint, key: key, authSecret: authSecret, user: app.user.uid }; // fetch('unregister') Override return socket.emit('plugins.pwa.unregister', subscription_object, function(err, data) { if (err) { // BIG USER FAIL ERROR! MORE CLOSER ATTENTION FOR HANDELING! console.log("[!] plugins.pwa.unregister ", err); return app.alertError(err.message); } console.log("[+] plugins.pwa.unregister received: ", data.received); }); // Original unregister code // return fetch('unregister', { // method: 'post', // headers: { // 'Content-type': 'application/json' // }, // body: JSON.stringify({ // endpoint: subscription.endpoint, // key: key, // authSecret: authSecret, // }) // }); }); }).then(setSubscribeButton); } if('serviceWorker' in navigator) { getSubscription().then(function(subscription){ if (subscription){ $('.subscriptionbtn').prop('checked', true); } else{ $('.subscriptionbtn').prop('checked', false); } }); } else{ console.log('{!Cli_SW} Error Subscribing '); } /** * Subscribe Button * If clicked then subscribe; should show disable if already subscribed * clicking on disabled button would unsubscribe and then need to click again * to resubscribe. **/ $('.subscriptionbtn').change(function() { console.log("[Cli_SW] Subscribe Button Clicked "); if($(this).prop('checked')){ //true subscribed console.log("[Cli_SW] Subscribe Button Subscribing"); subscribe(); } else{ //false unsubscribe console.log("[Cli_SW] Subscribe Button Unsubscribing"); unsubscribe(); } }); });
---- templates/partials/subscribe.tpl ---- <h4>Device Notifications</h4> <div class="well"> <div class="checkbox"> <div class="btn-group btn-toggle"> <button class="btn btn-sm btn-default">On</button> <button class="btn btn-sm btn-default active">Off</button> </div> <strong>Suscribe for device push notifications</strong> </div> <p class="help-block">If enabled, subscription to device notifications and recieve push messages sent from the application.</p> </div>
---- manifest.json src/controllers/index.js:303 ---- var manifest = { name: meta.config.title || 'NodeBB', start_url: nconf.get('relative_path') + '/', display: 'standalone', orientation: 'portrait', description: 'A description', background_color: '#ABC123', gcm_sender_id: '############', // I used firebase icons: [ { src: 'icon-192.png', sizes: '192x192', type: 'image/png' } ], related_applications: [ { platform: 'play', id: 'com.google.samples.apps.iosched' } ] }; if (meta.config['brand:touchIcon']) { manifest.icons.push({ src: nconf.get('relative_path') + '/uploads/system/touchicon-36.png', sizes: '36x36', type: 'image/png', density: 0.75 }, { src: nconf.get('relative_path') + '/uploads/system/touchicon-48.png', sizes: '48x48', type: 'image/png', density: 1.0 }, { src: nconf.get('relative_path') + '/uploads/system/touchicon-72.png', sizes: '72x72', type: 'image/png', density: 1.5 }, { src: nconf.get('relative_path') + '/uploads/system/touchicon-96.png', sizes: '96x96', type: 'image/png', density: 2.0 }, { src: nconf.get('relative_path') + '/uploads/system/touchicon-144.png', sizes: '144x144', type: 'image/png', density: 3.0 }, { src: nconf.get('relative_path') + '/uploads/system/touchicon-192.png', sizes: '192x192', type: 'image/png', density: 4.0 }); } res.status(200).json(manifest);
---- ServiceWorker.js ---- // a change in the cache name will force update on clients older SW version. Per google say so var CACHE_NAME = 'v1.00a'; /** * There must be an easier way to do this but automatic *.js creates session errors * Not implmented auto *.tpl, *.png etc. yet **/ var urlsToCache = [ // template caches & safe files '/assets/language/en-GB/login.json', '/assets/language/en-GB/user.json', '/assets/language/en-GB/register.json', '/assets/vendor/jquery/timeago/locales/jquery.timeago.en.js', '/assets/templates/alert.tpl', '/assets/templates/categories.tpl', '/assets/templates/category.tpl', '/assets/templates/chats.tpl', '/assets/templates/chat.tpl', '/assets/templates/confirm.tpl', '/assets/templates/footer.tpl', '/assets/templates/header.tpl', '/assets/templates/login.tpl', '/assets/templates/notifications.tpl', '/assets/templates/outgoing.tpl', '/assets/templates/popular.tpl', '/assets/templates/recent.tpl', '/assets/templates/registerComplete.tpl', '/assets/templates/register.tpl', '/assets/templates/reset_code.tpl', '/assets/templates/reset.tpl', '/assets/templates/search.tpl', '/assets/templates/tags.tpl', '/assets/templates/tag.tpl', '/assets/templates/topic.tpl', '/assets/templates/tos.tpl', '/assets/templates/new_chats.tpl', '/assets/templates/unread.tpl', '/assets/templates/users.tpl', '/assets/templates/partials/topic/badge.tpl', '/assets/templates/partials/topic/post-editor.tpl', '/assets/templates/partials/topic/post-menu-list.tpl', '/assets/templates/partials/topic/post-menu.tpl', '/assets/templates/partials/topic/post.tpl', '/assets/templates/partials/topic/quickreply.tpl', '/assets/templates/partials/topic/reply-button.tpl', '/assets/templates/partials/topic/sort.tpl', '/assets/templates/partials/topic/stats.tpl', '/assets/templates/partials/topic/topic-menu-list.tpl', '/assets/templates/partials/topic/watch.tpl', '/assets/templates/partials/noscript/warning.tpl', '/assets/templates/partials/modals/change_picture_modal.tpl', '/assets/templates/partials/modals/flag_post_modal.tpl', '/assets/templates/partials/modals/upload_file_modal.tpl', '/assets/templates/partials/modals/upload_picture_from_url_modal.tpl', '/assets/templates/partials/modals/votes_modal.tpl', '/assets/templates/partials/groups/list.tpl', '/assets/templates/partials/groups/memberlist.tpl', '/assets/templates/partials/chats/dropdown.tpl', '/assets/templates/partials/chats/messages.tpl', '/assets/templates/partials/chats/message.tpl', '/assets/templates/partials/chats/recent_room.tpl', '/assets/templates/partials/chats/user.tpl', '/assets/templates/partials/category/sort.tpl', '/assets/templates/partials/category/subcategory.tpl', '/assets/templates/partials/category/tags.tpl', '/assets/templates/partials/category/tools.tpl', '/assets/templates/partials/category/watch.tpl', '/assets/templates/partials/categories/item.tpl', '/assets/templates/partials/categories/lastpost.tpl', '/assets/templates/partials/categories/link.tpl', '/assets/templates/partials/account/header.tpl', '/assets/templates/partials/account/menu.tpl', '/assets/templates/account/edit/email.tpl', '/assets/templates/account/edit/password.tpl', '/assets/templates/account/edit/username.tpl', '/assets/templates/partials/acceptTos.tpl', '/assets/templates/partials/breadcrumbs.tpl', '/assets/templates/partials/category_list.tpl', '/assets/templates/partials/cookie-consent.tpl', '/assets/templates/partials/delete_posts_modal.tpl', '/assets/templates/partials/fork_thread_modal.tpl', '/assets/templates/partials/menu.tpl', '/assets/templates/partials/move_post_modal.tpl', '/assets/templates/partials/move_thread_modal.tpl', '/assets/templates/partials/notifications_list.tpl', '/assets/templates/partials/paginator.tpl', '/assets/templates/partials/post_bar.tpl', '/assets/templates/partials/posts_list.tpl', '/assets/templates/partials/sos_button.tpl', '/assets/templates/partials/subscribe.tpl', '/assets/templates/partials/tags_list.tpl', '/assets/templates/partials/thread_tools.tpl', '/assets/templates/partials/topics_list.tpl', '/assets/templates/partials/users_list_menu.tpl', '/assets/templates/partials/users_list.tpl', '/assets/templates/modules/taskbar.tpl', '/assets/templates/modules/usercard.tpl', '/assets/templates/groups/details.tpl', '/assets/templates/groups/list.tpl', '/assets/templates/groups/members.tpl', '/assets/templates/account/best.tpl', '/assets/templates/account/bookmarks.tpl', '/assets/templates/account/downvoted.tpl', '/assets/templates/account/edit.tpl', '/assets/templates/account/followers.tpl', '/assets/templates/account/following.tpl', '/assets/templates/account/groups.tpl', '/assets/templates/account/info.tpl', '/assets/templates/account/posts.tpl', '/assets/templates/account/profile.tpl', '/assets/templates/account/settings.tpl', '/assets/templates/account/topics.tpl', '/assets/templates/account/upvoted.tpl', '/assets/templates/account/watched.tpl', '/assets/uploads/system/favicon.ico', '/assets/uploads/system/site-logo.png', '/assets/uploads/system/touchicon-36.png', '/assets/uploads/system/touchicon-72.png', '/assets/uploads/system/touchicon-96.png', '/assets/uploads/system/touchicon-144.png', '/assets/uploads/system/touchicon-192.png', '/assets/uploads/system/touchicon-orig.png', ]; // Install the Service Worker self.addEventListener('install', function(event) { event.waitUntil( caches.open(CACHE_NAME).then(function(cache) { console.log('[ServiceWorker] Opened cache'); return cache.addAll(urlsToCache); }) ); }); // Listen to activation event self.addEventListener('activate', function(e) { //console.log('[ServiceWorker] Activate'); e.waitUntil( // Get all cache containers caches.keys().then(function(keyList) { return Promise.all(keyList.map(function(key) { // Check and remove invalid cache containers if (key !== CACHE_NAME) { console.log('[ServiceWorker] Removing old cache', key); return caches.delete(key); } })); }) ); // Enforce immediate scope control return self.clients.claim(); }); // Fetch from Cache /** * Good caching example to follow * https://github.com/14islands/vecka.14islands.com/blob/master/server/service-worker.js **/ self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request).then(function(response) { // Cache hit - return response if (response) { //console.log('Fetched a cached response: ' + event.request.url); return response; } // IMPORTANT: Clone the request. A request is a stream and // can only be consumed once. Since we are consuming this // once by cache and once by the browser for fetch, we need // to clone the response. var fetchRequest = event.request.clone(); return fetch(fetchRequest).then(function(response) { // Check if we received a valid response if(!response || response.status !== 200 || response.type !== 'basic') { return response; } // IMPORTANT: Clone the response. A response is a stream // and because we want the browser to consume the response // as well as the cache consuming the response, we need // to clone it so we have two streams. var responseToCache = response.clone(); var requestedURL = event.request.url; // If; the request is something that causes an error in nodeBB to cache or an error in general; forward to server if(event.request.method=='POST' || requestedURL.includes('socket') || requestedURL.includes('widgets/render') || requestedURL.includes('admin') ){ return response; } // Else; open put the request into the cache and serve from there caches.open(CACHE_NAME).then(function(cache) { //console.log('Fetching a response: ' + event.request.url); cache.put(event.request, responseToCache); }); return response; }); }) ); }); /** * Listen to pushsubscriptionchange event which is fired when subscription expires. * Subscribe again and register the new subscription in the server by sending a POST request with endpoint. * Real world application would probably use also user identification. **/ self.addEventListener('pushsubscriptionchange', function(event) { console.log('[ServiceWorker] Subscription expired'); event.waitUntil( self.registration.pushManager.subscribe({ userVisibleOnly: true }) .then(function(subscription) { console.log('[ServiceWorker] Subscribed after expiration', subscription.endpoint); return fetch('register', { method: 'post', headers: { 'Content-type': 'application/json' }, body: JSON.stringify({ endpoint: subscription.endpoint }) }); }) ); }); /** * Push notifications * Register event listener for the โpushโ event. * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification **/ self.addEventListener('push', function(event) { // Retrieve the textual payload from event.data (a PushMessageData object). var payload = event.data ? event.data.text() : 'no payload'; // Show the Notification // Keep the service worker alive until the notification is created. // By checking the payload (string compare), different notifications can be displayed! event.waitUntil( // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification // Show a notification with title โTitleโ and use the payload as the body. // Vibration is on: [Vibrate] [Delay] ... scheme /** * This decision to ignore if app is already open, is to send a notification regardless of app state **/ self.registration.showNotification('SOS', { body: payload, icon: '/assets/uploads/system/touchicon-orig.png', // icon: app.user.picture, // !!! make SW cache all user avatars vibrate: [200, 100, 200, 100], // (...---...) V_200,P_100,V_200,P_100 ... //tag: 'only_one', // Having a tag here will replace other notifications sent, instead of having different ones appear actions: [ {action: 'postpone', title: 'Automate Response'}, {action: 'respond', title: 'Open Chat'} ], }) ); }); /** * List for when the notification is clicked * https://developers.google.com/web/fundamentals/engage-and-retain/push-notifications/ **/ self.addEventListener('notificationclick', event => { var appUrl=''; if (event.action === 'respond') { // Goto the webchat directly console.log('[SW] -respond- clicked'); // Grab a certain thing from the server based on action appUrl = '/'; } else if (event.action === 'postpone') { // don't open the app, send a premade response to the chatroom console.log('[SW] -postponed- clicked'); // Grab a certain thing from the server based on action appUrl = '/'; } else { // Just open the app. console.log('[SW] -notification- clicked'); // Grab a certain thing from the server based on action appUrl = '/'; } // gather a window, open the appUrl. event.waitUntil(clients.matchAll({ includeUncontrolled: true, type: 'window' }).then( activeClients => { if (activeClients.length > 0) { activeClients[0].navigate(appUrl); activeClients[0].focus(); } else { clients.openWindow(appUrl); } }) ); // Do something with the event event.notification.close(); }); self.addEventListener('notificationclose', event => { // Do something with the event console.log('[SW] notification closed'); });
The tpl is from another theme plugin but the rest is all on the same plugin. And I didn't really do much with plugin.json although I will now. Any other simplification pointers would be great as well.
Other thing in case you didn't already know for your app to be recognized for sw registration first it has to use https, says google. iOS doesn't have much support, safari on mac barely makes an effort to utilize service workers.
Hope I've been helpful
-
@kurt3rd Thanks a lot, man. Awesome stuff.
-
This post is deleted!
-
@kurt3rd said in NodeBB PWA:
groovy
@meetdilip I want help regrading PWA
I want that , when somebody post in a specific category , person should get notification in his mobile. I really exicted about PWA . Plez help me out
-
@abhinov-singh it's not something you can do yourself. If it was, you'd already know about it.
-
PWA are good for your client's business. Alibaba, the Chinese Amazon, notices a 48% increase in user engagement thanks to the browser's prompt to "install" the website (source).
This makes the effort totally worth fighting for !
This bounty is possible thanks to a technology called Service Workers that allows you to save static assets in the user system (html,css, javascript,json...), alongside a manifest.json that specifies how the website should behave as an installed application.
-
@sanatisharif yes i know what a PWA is. It's a lot of work to implement it, and it's not the highest priority for me.
-
We implement many of the best practices for delivering a good mobile app experience, but we are not fully PWA yet. Unfortunately, we do not have a timeline for this feature, but I'd recommend opening up a new issue on GitHub for discussing this