From ada4688c4e0bc2a6adcdd757821d1779ed4cd570 Mon Sep 17 00:00:00 2001 From: Rosa Gutierrez Date: Mon, 5 Jan 2026 12:10:57 +0100 Subject: [PATCH] Take a first stab at offline mode support --- Gemfile.lock | 4 +- Gemfile.saas.lock | 14 +++-- app/javascript/initializers/index.js | 1 + app/javascript/initializers/offline.js | 6 ++ app/views/pwa/service_worker.js | 41 -------------- app/views/pwa/service_worker.js.erb | 76 ++++++++++++++++++++++++++ config/importmap.rb | 1 + 7 files changed, 96 insertions(+), 47 deletions(-) create mode 100644 app/javascript/initializers/offline.js delete mode 100644 app/views/pwa/service_worker.js create mode 100644 app/views/pwa/service_worker.js.erb diff --git a/Gemfile.lock b/Gemfile.lock index 3a35c35c7..a710d0f9e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,10 +13,10 @@ GIT GIT remote: https://github.com/hotwired/turbo-rails.git - revision: e3795ef6aac6a3d18039751febe6a301cfe71a7b + revision: 49be3f2d43edeeedad48cb492b35242fdb27af74 branch: offline-cache specs: - turbo-rails (2.0.20) + turbo-rails (2.0.21) actionpack (>= 7.1.0) railties (>= 7.1.0) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 152d1889c..cec5be07b 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -60,6 +60,15 @@ GIT rails (>= 6.1) yabeda (~> 0.6) +GIT + remote: https://github.com/hotwired/turbo-rails.git + revision: 49be3f2d43edeeedad48cb492b35242fdb27af74 + branch: offline-cache + specs: + turbo-rails (2.0.21) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + GIT remote: https://github.com/rails/rails.git revision: 12e24eaf2f0a9613e015653f013dd131317d9bf5 @@ -633,9 +642,6 @@ GEM trilogy (2.10.0) bigdecimal tsort (0.2.0) - turbo-rails (2.0.23) - actionpack (>= 7.1.0) - railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (3.2.0) @@ -752,7 +758,7 @@ DEPENDENCIES stripe (~> 18.0) thruster trilogy (~> 2.10) - turbo-rails + turbo-rails! useragent! vcr web-console! diff --git a/app/javascript/initializers/index.js b/app/javascript/initializers/index.js index 90ef36a26..1d16ef205 100644 --- a/app/javascript/initializers/index.js +++ b/app/javascript/initializers/index.js @@ -1,2 +1,3 @@ import "initializers/current" import "initializers/bridge/bridge_element" +import "initializers/offline" diff --git a/app/javascript/initializers/offline.js b/app/javascript/initializers/offline.js new file mode 100644 index 000000000..c5eff824e --- /dev/null +++ b/app/javascript/initializers/offline.js @@ -0,0 +1,6 @@ +import { Turbo } from "@hotwired/turbo-rails" + +Turbo.offline.start("/service-worker.js", { + scope: "/", + native: true +}) diff --git a/app/views/pwa/service_worker.js b/app/views/pwa/service_worker.js deleted file mode 100644 index 91903e735..000000000 --- a/app/views/pwa/service_worker.js +++ /dev/null @@ -1,41 +0,0 @@ -self.addEventListener('fetch', (event) => { - if (event.request.method !== 'GET') return - - if (event.request.destination === 'document') { - event.respondWith( - fetch(event.request, { cache: 'no-cache' }) - .catch(() => caches.match(event.request)) - ) - } -}) - -self.addEventListener("push", (event) => { - const data = event.data.json() - event.waitUntil(Promise.all([ showNotification(data), updateBadgeCount(data.options) ])) -}) - -async function showNotification({ title, options }) { - return self.registration.showNotification(title, options) -} - -async function updateBadgeCount({ data: { badge } }) { - return self.navigator.setAppBadge?.(badge || 0) -} - -self.addEventListener("notificationclick", (event) => { - event.notification.close() - - const url = new URL(event.notification.data.url, self.location.origin).href - event.waitUntil(openURL(url)) -}) - -async function openURL(url) { - const clients = await self.clients.matchAll({ type: "window" }) - const focused = clients.find((client) => client.focused) - - if (focused) { - await focused.navigate(url) - } else { - await self.clients.openWindow(url) - } -} diff --git a/app/views/pwa/service_worker.js.erb b/app/views/pwa/service_worker.js.erb new file mode 100644 index 000000000..fb583658c --- /dev/null +++ b/app/views/pwa/service_worker.js.erb @@ -0,0 +1,76 @@ +importScripts("<%= javascript_url("turbo-offline-umd.min") %>") + +TurboOffline.addRule({ + match: /\/assets\/.+[-\.][0-9a-f]+\.(js|css|svg|png|jpg|webp|woff2?|ico)$/, + handler: TurboOffline.handlers.cacheFirst({ + cacheName: "assets", + maxAge: 60 * 60 * 24 * 7 + }) +}) + +TurboOffline.addRule({ + match: (request) => { + const url = request.url + return /\/(boards|cards|users)\//.test(url) && !/\/(edit|pin|watch|new)$/.test(url) + }, + handler: TurboOffline.handlers.networkFirst({ + cacheName: "cards", + maxAge: 60 * 60 * 24 * 3, + networkTimeout: 3 + }) +}) + +TurboOffline.addRule({ + match: /\/rails\/active_storage\//, + handler: TurboOffline.handlers.networkFirst({ + cacheName: "storage", + maxAge: 60 * 60 * 24 * 7, + networkTimeout: 3 + }) +}) + +// Everything else +TurboOffline.addRule({ + handler: TurboOffline.handlers.networkFirst({ + cacheName: "misc", + maxAge: 60 * 60 * 24, + networkTimeout: 3 + }) +}) + +TurboOffline.start() + +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener("push", (event) => { + const data = event.data.json() + event.waitUntil(Promise.all([ showNotification(data), updateBadgeCount(data.options) ])) +}) + +async function showNotification({ title, options }) { + return self.registration.showNotification(title, options) +} + +async function updateBadgeCount({ data: { badge } }) { + return self.navigator.setAppBadge?.(badge || 0) +} + +self.addEventListener("notificationclick", (event) => { + event.notification.close() + + const url = new URL(event.notification.data.url, self.location.origin).href + event.waitUntil(openURL(url)) +}) + +async function openURL(url) { + const clients = await self.clients.matchAll({ type: "window" }) + const focused = clients.find((client) => client.focused) + + if (focused) { + await focused.navigate(url) + } else { + await self.clients.openWindow(url) + } +} diff --git a/config/importmap.rb b/config/importmap.rb index 5da84683c..f6910c3e1 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -2,6 +2,7 @@ pin "application" pin "@hotwired/turbo-rails", to: "turbo.min.js" +pin "@hotwired/turbo/offline", to: "turbo-offline.min.js" pin "@hotwired/stimulus", to: "stimulus.min.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin "@hotwired/hotwire-native-bridge", to: "@hotwired--hotwire-native-bridge.js"