FEATURE: Make Discourse work offline with WorkboxJS (#7870)

FEATURE: Make Discourse work offline with WorkboxJS (#7870)

diff --git a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6
index b78044e..92ae988 100644
--- a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6
+++ b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6
@@ -9,9 +9,7 @@ export default {
     const isSupported = isSecured && "serviceWorker" in navigator;
 
     if (isSupported) {
-      const isApple = !!navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i);
-
-      if (Discourse.ServiceWorkerURL && !isApple) {
+      if (Discourse.ServiceWorkerURL) {
         navigator.serviceWorker.getRegistrations().then(registrations => {
           for (let registration of registrations) {
             if (
diff --git a/app/assets/javascripts/discourse/lib/ajax.js.es6 b/app/assets/javascripts/discourse/lib/ajax.js.es6
index e2f28ce..e4ca8df 100644
--- a/app/assets/javascripts/discourse/lib/ajax.js.es6
+++ b/app/assets/javascripts/discourse/lib/ajax.js.es6
@@ -140,7 +140,7 @@ export function ajax() {
     }
 
     if (args.type === "GET" && args.cache !== true) {
-      args.cache = false;
+      args.cache = true; // Disable JQuery cache busting param, which was created to deal with IE8
     }
 
     ajaxObj = $.ajax(Discourse.getURL(url), args);
diff --git a/app/assets/javascripts/service-worker.js.erb b/app/assets/javascripts/service-worker.js.erb
index 50fb942..56d5eb7 100644
--- a/app/assets/javascripts/service-worker.js.erb
+++ b/app/assets/javascripts/service-worker.js.erb
@@ -1,117 +1,25 @@
 'use strict';
 
+importScripts("<%= ::UrlHelper.absolute("/javascripts/workbox/workbox-sw.js") %>");
 
-// Special offline and fetch interception is restricted to Android only
-// we have had a large amount of pain supporting this on Firefox / Safari
-// it is only strongly required on Android, when PWA gets better on iOS
-// we can unlock it there as well, for Desktop we can consider unlocking it
-// if we start supporting offline browsing for laptops
-if (/(android)/i.test(navigator.userAgent)) {
-
-  // Incrementing CACHE_VERSION will kick off the install event and force previously cached
-  // resources to be cached again.
-  const CACHE_VERSION = 1;
-
-  const CURRENT_CACHES = {
-    offline: 'offline-v' + CACHE_VERSION
-  };
-
-  const OFFLINE_URL = 'offline.html';
-
-  const createCacheBustedRequest = function(url) {
-    var headers = new Headers({
-      'Discourse-Track-View': '0'
-    });
-
-    var request = new Request(url, {cache: 'reload', headers: headers});
-    // See https://fetch.spec.whatwg.org/#concept-request-mode
-    // This is not yet supported in Chrome as of M48, so we need to explicitly check to see
-    // if the cache: 'reload' option had any effect.
-    if ('cache' in request) {
-      return request;
-    }
-
-    // If {cache: 'reload'} didn't have any effect, append a cache-busting URL parameter instead.
-    var bustedUrl = new URL(url, self.location.href);
-    bustedUrl.search += (bustedUrl.search ? '&' : '') + 'cachebust=' + Date.now();
-    return new Request(bustedUrl, {headers: headers});
-  }
-
-  self.addEventListener('install', function(event) {
-    event.waitUntil(
-      // We can't use cache.add() here, since we want OFFLINE_URL to be the cache key, but
-      // the actual URL we end up requesting might include a cache-busting parameter.
-      fetch(createCacheBustedRequest(OFFLINE_URL)).then(function(response) {
-        return caches.open(CURRENT_CACHES.offline).then(function(cache) {
-          return cache.put(OFFLINE_URL, response);
-        });
-      }).then(function(cache) {
-        self.skipWaiting();
-      })
-    );
-  });
-
-  self.addEventListener('activate', function(event) {
-    // Delete all caches that aren't named in CURRENT_CACHES.
-    // While there is only one cache in this example, the same logic will handle the case where
-    // there are multiple versioned caches.
-    var expectedCacheNames = Object.keys(CURRENT_CACHES).map(function(key) {
-      return CURRENT_CACHES[key];
-    });
-
-    event.waitUntil(
-      caches.keys().then(function(cacheNames) {
-        return Promise.all(
-          cacheNames.map(function(cacheName) {
-            if (expectedCacheNames.indexOf(cacheName) === -1) {
-              // If this cache name isn't present in the array of "expected" cache names,
-              // then delete it.
-              return caches.delete(cacheName);
-            }
-          })
-        );
-      }).then(function() {
-        self.clients.claim()
-      })
-    );
-  });
-
-  self.addEventListener('fetch', function(event) {
-    // Bypass service workers if this is a url with a token param
-    if(/\?.*token/i.test(event.request.url)) {
-      return;
-    }
-    // We only want to call event.respondWith() if this is a navigation request
-    // for an HTML page.
-    // request.mode of 'navigate' is unfortunately not supported in Chrome
-    // versions older than 49, so we need to include a less precise fallback,
-    // which checks for a GET request with an Accept: text/html header.
-    if (event.request.mode === 'navigate' ||
-        (event.request.method === 'GET' &&
-         event.request.headers.get('accept').includes('text/html'))) {
-      event.respondWith(
-        fetch(event.request).catch(function(error) {
-          // The catch is only triggered if fetch() throws an exception, which will most likely
-          // happen due to the server being unreachable.
-          // If fetch() returns a valid HTTP response with an response code in the 4xx or 5xx
-          // range, the catch() will NOT be called. If you need custom handling for 4xx or 5xx
-          // errors, see https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker/fallback-response
-          if (!navigator.onLine) {
-            return caches.match(OFFLINE_URL);
-          } else {
-            throw new Error(error);
-          }
-        })
-      );
-    }
-
-    // If our if() condition is false, then this fetch handler won't intercept the request.
-    // If there are any other fetch handlers registered, they will get a chance to call
-    // event.respondWith(). If no fetch handlers call event.respondWith(), the request will be
-    // handled by the browser as if there were no service worker involvement.
-  });
+workbox.setConfig({
+  modulePathPrefix: "<%= ::UrlHelper.absolute("/javascripts/workbox") %>"
+});
 
-}
+const cacheVersion = "1";
+
+// Cache all GET requests, so Discourse can be used while offline
+workbox.routing.registerRoute(
+  new RegExp('.*?'), // Matches all, GET is implicit
+  new workbox.strategies.NetworkFirst({ // This will only use the cache when a network request fails
+    cacheName: "discourse-" + cacheVersion,
+    plugins: [
+      new workbox.expiration.Plugin({
+        maxAgeSeconds: 7* 24 * 60 * 60, // 7 days
+      }),
+    ],
+  })
+);
 
 const idleThresholdTime = 1000 * 10; // 10 seconds
 var lastAction = -1;
diff --git a/lib/tasks/javascript.rake b/lib/tasks/javascript.rake
index f933c4b..f66c6af 100644
--- a/lib/tasks/javascript.rake
+++ b/lib/tasks/javascript.rake
@@ -90,6 +90,26 @@ task 'javascript:update' do
     }, {
       # TODO: drop when we eventually drop IE11, this will land in iOS in version 13
       source: 'intersection-observer/intersection-observer.js'
+    }, {
+      source: 'workbox-sw/build/.',
+      destination: 'workbox',
+      public: true
+    }, {
+      source: 'workbox-routing/build/.',
+      destination: 'workbox',
+      public: true
+    }, {
+      source: 'workbox-core/build/.',
+      destination: 'workbox',
+      public: true
+    }, {
+      source: 'workbox-strategies/build/.',
+      destination: 'workbox',
+      public: true
+    }, {
+      source: 'workbox-expiration/build/.',
+      destination: 'workbox',
+      public: true
     }
   ]
 
diff --git a/package.json b/package.json
index 43aee39..c4bce5a 100644
--- a/package.json

[... diff too long, it was truncated ...]

GitHub sha: 1221d342

Can we add some indicator to say that we’re in “Offline Mode”. Right now there is no way to tell - I refresh latest and think “oh, there are no new topics”, when actually it is because I lost network connectivity.

3 Likes

Following it up on Offline Indicator - ux - Discourse Meta

1 Like