NodeBB PWA


  • GNU/Linux

    Is there any chance of a PWA version availability for NodeBB ? Progressive Web Apps ( PWA ) are getting very popular these days for it's cross platform usability.


  • Global Moderator

    @meetdilip NodeBB already tries to be as progressive as possible.

    PWA isn't a thing. What I mean is that "progressive web apps" is really just a name for any website that supports a certain set of features and behaviors.

    But yes, being more progressive and using the technologies available to provide the best user experience everywhere is something NodeBB tries to do.


  • GNU/Linux

    I don't know if I am correcting you. But, on an Android mobile, a PWA works just like an app, using minimal resources. For eg, you visit this forum on mobile browser, it gives the user an option to save the " app " which is a web app to mobile home screen. Though it acts like a bookmark, there are options like notifications, uninstall etc available.

    You would need to add a Service Worker and upload some SDK specific files to web server to achieve this.


  • Global Moderator

    @meetdilip Feel free to open an issue on the Github repo if you wish for us to implement these features. Thanks.


  • GNU/Linux

    I can. Only if there is enough interest in this :)



  • Personally, I never use a mobile app if there already is a good mobile website so this doesn't seem useful to me.


  • Global Moderator

    @Telokis well this just makes the website and app better by adding push notifications through web APIs etc:cloud:


  • Admin

    You can already add a site to your home screen from Chrome (and likely via other browsers), so there's that. There are more ways to get deeper integration with use of manifests and application caches, but we're not sure we want to go down that road right now :smile:


  • GNU/Linux

    Google calls it the future of apps. All you have to do is upload a few things to your server. You are saved from making a separate app for your web service.


  • GNU/Linux

    @PitaJ Actually, it makes a web app behave like a mobile app. Yes, push notifications will also be there.

    A good read for you


  • Anime Lovers

    Not one website I use is a PWA as far as I can tell.


  • GNU/Linux

    Not a single free software was responsive when NodeBB was introduced as well ;)


  • Admin

    @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 :stuck_out_tongue:



  • 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


  • Global Moderator

    @kurt3rd

    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.


  • Global Moderator

    @kurt3rd

    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 command :)

    I 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 :)


  • Global Moderator

    @kurt3rd Thanks a lot, man. Awesome stuff.


Log in to reply
 

Looks like your connection to NodeBB was lost, please wait while we try to reconnect.