Merge pull request #2582 from basecamp/offline-mode
Offline mode for visited/loaded resources
This commit is contained in:
@@ -8,7 +8,7 @@ gem "rails", github: "rails/rails", branch: "main"
|
||||
gem "importmap-rails"
|
||||
gem "propshaft"
|
||||
gem "stimulus-rails"
|
||||
gem "turbo-rails"
|
||||
gem "turbo-rails", github: "hotwired/turbo-rails", branch: "offline-cache"
|
||||
|
||||
# Deployment and drivers
|
||||
gem "bootsnap", require: false
|
||||
|
||||
+10
-4
@@ -11,6 +11,15 @@ GIT
|
||||
specs:
|
||||
useragent (0.16.11)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/hotwired/turbo-rails.git
|
||||
revision: 93ce5bc461cd71af1797e79fcd7e09a19242d080
|
||||
branch: offline-cache
|
||||
specs:
|
||||
turbo-rails (2.0.23)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rails/rails.git
|
||||
revision: 12e24eaf2f0a9613e015653f013dd131317d9bf5
|
||||
@@ -461,9 +470,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)
|
||||
@@ -539,7 +545,7 @@ DEPENDENCIES
|
||||
stimulus-rails
|
||||
thruster
|
||||
trilogy (~> 2.10)
|
||||
turbo-rails
|
||||
turbo-rails!
|
||||
useragent!
|
||||
vcr
|
||||
web-console!
|
||||
|
||||
+10
-4
@@ -60,6 +60,15 @@ GIT
|
||||
rails (>= 6.1)
|
||||
yabeda (~> 0.6)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/hotwired/turbo-rails.git
|
||||
revision: 93ce5bc461cd71af1797e79fcd7e09a19242d080
|
||||
branch: offline-cache
|
||||
specs:
|
||||
turbo-rails (2.0.23)
|
||||
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!
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { Turbo } from "@hotwired/turbo-rails"
|
||||
|
||||
export default class extends Controller {
|
||||
clearCache() {
|
||||
Turbo.offline.clearCache()
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export default class extends Controller {
|
||||
|
||||
submit() {
|
||||
if (this.inputTarget.disabled) return
|
||||
this.element.submit()
|
||||
this.element.requestSubmit()
|
||||
this.inputTarget.disabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
import "initializers/current"
|
||||
import "initializers/bridge/bridge_element"
|
||||
import "initializers/offline"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Turbo } from "@hotwired/turbo-rails"
|
||||
|
||||
if (Current.user) {
|
||||
Turbo.offline.start("/service-worker.js", {
|
||||
scope: "/",
|
||||
native: true,
|
||||
preload: /\/assets\//
|
||||
})
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<%= tag.li class: "popup__item", data: { filter_target: "item", navigable_list_target: "item" } do %>
|
||||
<%= icon_tag "logout", class: "popup__icon" %>
|
||||
<%= button_to session_path(script_name: nil), method: :delete, class: "popup__btn btn", data: { turbo: false } do %>
|
||||
<%= button_to session_path(script_name: nil), method: :delete, class: "popup__btn btn", form: { data: { turbo: false, controller: "clear-offline-cache", action: "submit->clear-offline-cache#clearCache" } } do %>
|
||||
<span>Sign out</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
importScripts("<%= javascript_url("turbo-offline-umd.min") %>")
|
||||
|
||||
// Documents - top-level navigation requests only
|
||||
// We need `cache: "no-cache"` here to work around
|
||||
// an annoying Safari PWA bug that predates offline mode
|
||||
TurboOffline.addRule({
|
||||
match: (request) => request.destination === "document",
|
||||
except: /\/(edit|pin|watch|new)$/,
|
||||
handler: TurboOffline.handlers.networkFirst({
|
||||
cacheName: "main",
|
||||
maxAge: 60 * 60 * 24 * 3,
|
||||
networkTimeout: 3,
|
||||
fetchOptions: { cache: "no-cache" },
|
||||
maxEntrySize: 1024 * 1024
|
||||
})
|
||||
})
|
||||
|
||||
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,
|
||||
maxEntrySize: 1024 * 1024
|
||||
})
|
||||
})
|
||||
|
||||
TurboOffline.addRule({
|
||||
match: /\/(boards|cards|users)\//,
|
||||
except: /\/(edit|pin|watch|new)$/,
|
||||
handler: TurboOffline.handlers.networkFirst({
|
||||
cacheName: "main",
|
||||
maxAge: 60 * 60 * 24 * 3,
|
||||
networkTimeout: 2,
|
||||
maxEntrySize: 1024 * 1024
|
||||
})
|
||||
})
|
||||
|
||||
TurboOffline.addRule({
|
||||
match: /\/rails\/active_storage\//,
|
||||
handler: TurboOffline.handlers.networkFirst({
|
||||
cacheName: "storage",
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
networkTimeout: 2,
|
||||
maxEntrySize: 2 * 1024 * 1024, // 2MB covers about 95% of all Fizzy blobs
|
||||
maxEntries: 500,
|
||||
fetchOptions: { mode: "cors" }
|
||||
})
|
||||
})
|
||||
|
||||
// Everything else
|
||||
TurboOffline.addRule({
|
||||
except: /\/(service-worker\.js|edit|pin|watch|new)$/,
|
||||
handler: TurboOffline.handlers.networkFirst({
|
||||
cacheName: "main",
|
||||
maxAge: 60 * 60 * 24,
|
||||
networkTimeout: 3,
|
||||
maxEntrySize: 1024 * 1024
|
||||
})
|
||||
})
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
TurboOffline.start()
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: "magic-link" } } do |form| %>
|
||||
<%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: "magic-link clear-offline-cache", action: "submit->clear-offline-cache#clearCache" } } do |form| %>
|
||||
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large txt-uppercase",
|
||||
autofocus: true, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true,
|
||||
autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code],
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<% if Current.user == @user %>
|
||||
<hr class="separator--horizontal full-width flex-item-grow margin-block-start-double" style="--border-color: var(--color-ink-light);" aria-hidden="true">
|
||||
|
||||
<%= button_to session_path(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %>
|
||||
<%= button_to session_path(script_name: nil), method: :delete, class: "btn txt-x-small center", form: { data: { turbo: false, controller: "clear-offline-cache", action: "submit->clear-offline-cache#clearCache" } } do %>
|
||||
<span>Sign out of Fizzy on this device</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user