Revert "Revert "Removed service worker (#12974)" (#13014)" (#13015)

This reverts commit 2416387fb4.
This commit is contained in:
Nick Taylor 2021-03-17 10:57:56 -04:00 committed by GitHub
parent 4e997e7e3a
commit 7c1343be9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 125 additions and 795 deletions

View file

@ -1,5 +1,4 @@
# Development options
SKIP_SERVICEWORKERS="true"
# App core values
APP_DOMAIN="localhost:3000"

View file

@ -5,7 +5,6 @@
//= require utilities/getImageForLink
//= require honeybadger-js/dist/honeybadger.js
//= require ahoy.js/dist/ahoy.js
//= require serviceworker-companion
var instantClick
, InstantClick = instantClick = function(document, location, $userAgent) {

View file

@ -1,36 +0,0 @@
'use strict';
if ('serviceWorker' in navigator) {
// Safari has issues with the service worker, so we'll just skip it on those browsers, and unregister it if it's loaded
if (
/Safari/i.test(navigator.userAgent) &&
/Apple Computer/.test(navigator.vendor)
) {
//unregister serviceworker
navigator.serviceWorker
.getRegistration('/serviceworker.js')
.then(function (registration) {
if (registration) {
registration.unregister();
}
});
} else {
navigator.serviceWorker
.register('/serviceworker.js', { scope: '/' })
.then(function swStart(registration) {
// registered!
})
.catch((error) => {
// eslint-disable-next-line no-console
console.log('ServiceWorker registration failed: ', error);
});
}
}
window.addEventListener('beforeinstallprompt', (e) => {
// beforeinstallprompt Event fired
// e.userChoice will return a Promise.
e.userChoice.then((choiceResult) => {
ga('send', 'event', 'PWA-install', choiceResult.outcome);
});
});

View file

@ -41,14 +41,8 @@ class ApplicationController < ActionController::Base
health_checks].freeze
private_constant :PUBLIC_CONTROLLERS
# TODO: Remove the "shell" endpoints, because they are for service worker
# functionality we no longer need. We are keeping these around mid-March
# 2021 because previously-installed service workers may still expect them.
CONTENT_CHANGE_PATHS = [
"/tags/onboarding", # Needs to change when suggested_tags is edited.
"/shell_top", # Cached at edge, sent to service worker.
"/shell_bottom", # Cached at edge, sent to service worker.
"/async_info/shell_version", # Checks if current users should be busted.
"/onboarding", # Page is cached at edge.
"/", # Page is cached at edge.
].freeze

View file

@ -83,8 +83,7 @@ class PagesController < ApplicationController
end
def report_abuse
referer = URL.sanitized_referer(request.referer)
reported_url = params[:reported_url] || params[:url] || referer
reported_url = params[:reported_url] || params[:url] || request.referer.presence
@feedback_message = FeedbackMessage.new(
reported_url: reported_url&.chomp("?i=i"),
)

View file

@ -5,8 +5,7 @@ class RegistrationsController < Devise::RegistrationsController
if user_signed_in?
redirect_to root_path(signin: "true")
else
referer_path = URI(request.referer || "").path
if URI(request.referer || "").host == URI(request.base_url).host && referer_path != "/serviceworker.js"
if URI(request.referer || "").host == URI(request.base_url).host
store_location_for(:user, request.referer)
end
super

View file

@ -1,14 +1,9 @@
class ServiceWorkerController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :set_cache_control_headers, only: %i[index manifest]
before_action :set_cache_control_headers, only: %i[index]
def index
set_surrogate_key_header "serviceworker-js"
render formats: [:js]
end
def manifest
set_surrogate_key_header "manifest-json"
render formats: [:json]
end
end

View file

@ -261,10 +261,6 @@ module ApplicationHelper
URL.organization(organization)
end
def sanitized_referer(referer)
URL.sanitized_referer(referer)
end
def sanitize_and_decode(str)
# using to_str instead of to_s to prevent removal of html entity code
HTMLEntities.new.decode(sanitize(str).to_str)

View file

@ -1,7 +1,5 @@
# Utilities methods to safely build app wide URLs
module URL
SERVICE_WORKER = "/serviceworker.js".freeze
def self.protocol
ApplicationConfig["APP_PROTOCOL"]
end
@ -72,19 +70,4 @@ module URL
def self.organization(organization)
url(organization.slug)
end
# Ensures we don't consider serviceworker.js as referer
#
# @param referer [String] the unsanitized referer
# @example A safe referer
# sanitized_referer("/some/path") #=> "/some/path"
# @example serviceworker.js as referer
# sanitized_referer("serviceworker.js") #=> nil
# @example An empty string
# sanitized_referer("") #=> nil
def self.sanitized_referer(referer)
return if referer.blank? || URI(referer).path == SERVICE_WORKER
referer
end
end

View file

@ -48,7 +48,8 @@
<% end %>
<% if internal_navigation? %>
<!-- If you have service workers turned on, you may be seeing the canonical URL in the body for human inspection -->
<!-- You may be seeing the canonical URL in the body for human inspection -->
<!-- This is because of navigation via InstantClick, instantclick.io -->
<!-- However, the crawled version of the page has the canonical in the head. -->
<!-- This is confirmed by the canonical url inspector. See https://github.com/forem/forem/issues/9509#issuecomment-732259221. -->
<link rel="canonical" href="<%= @article.canonical_url.presence || app_url(@article.path) %>" />

View file

@ -1,4 +1,86 @@
<%= render "shell/top" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<% title = yield(:title) %>
<title><%= title || community_name.to_s %></title>
<% unless internal_navigation? %>
<meta name="last-updated" content="<%= Time.current %>">
<meta name="user-signed-in" content="<%= user_signed_in? %>">
<meta name="head-cached-at" content="<%= Time.current.to_i %>">
<% if SiteConfig.payment_pointer.present? %>
<!-- Experimental web monetization payment pointer for micropayments -->
<!-- It lets readers make micropayments to websites they visit. -->
<!-- This is step 1: Get live in production to test for platform-wide payment pointer. -->
<!-- Step 2: Allow authors to set their payment pointer so they can directly monetize their content based on visitors. -->
<!-- Step 3: Enable further functionality based on what we learn from this experimentation and how the ecosystem evolves. -->
<meta name="monetization" content="<%= SiteConfig.payment_pointer %>">
<% end %>
<meta name="environment" content="<%= Rails.env %>">
<%= render "layouts/styles", qualifier: "main" %>
<% unless user_signed_in? %>
<%= javascript_packs_with_chunks_tag "base", "Search", defer: true %>
<% end %>
<%= javascript_include_tag "base", defer: true %>
<% if user_signed_in? %>
<%= javascript_packs_with_chunks_tag "base", "Search", "onboardingRedirectCheck", "contentDisplayPolicy", defer: true %>
<% end %>
<%= yield(:page_meta) %>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<%= favicon_link_tag Images::Optimizer.call(SiteConfig.favicon_url, width: 32) %>
<link rel="apple-touch-icon" href="<%= optimized_image_url(SiteConfig.logo_png, width: 180, fetch_format: "png") %>">
<link rel="apple-touch-icon" sizes="152x152" href="<%= optimized_image_url(SiteConfig.logo_png, width: 152, fetch_format: "png") %>">
<link rel="apple-touch-icon" sizes="180x180" href="<%= optimized_image_url(SiteConfig.logo_png, width: 180, fetch_format: "png") %>">
<link rel="apple-touch-icon" sizes="167x167" href="<%= optimized_image_url(SiteConfig.logo_png, width: 167, fetch_format: "png") %>">
<link href="<%= optimized_image_url(SiteConfig.logo_png, width: 192, fetch_format: "png") %>" rel="icon" sizes="192x192" />
<link href="<%= optimized_image_url(SiteConfig.logo_png, width: 128, fetch_format: "png") %>" rel="icon" sizes="128x128" />
<meta name="apple-mobile-web-app-title" content="<%= SiteConfig.app_domain %>">
<meta name="application-name" content="<%= SiteConfig.app_domain %>">
<meta name="theme-color" content="#000000" />
<link rel="search" href="<%= URL.url("open-search.xml") %>" type="application/opensearchdescription+xml" title="<%= community_name %>" />
<meta property="forem:name" content="<%= community_name %>" />
<meta property="forem:logo" content="<%= optimized_image_url(SiteConfig.logo_png, width: 512, fetch_format: "png") %>" />
<meta property="forem:domain" content="<%= SiteConfig.app_domain %>" />
<% end %>
</head>
<% unless internal_navigation? %>
<% cache(release_adjusted_cache_key("top-html-and-config--#{user_signed_in?}")) do %>
<body
data-user-status="<%= user_logged_in_status %>"
class="<%= SiteConfig.default_font.tr("_", "-") %>-article-body default-header"
data-pusher-key="<%= ApplicationConfig["PUSHER_KEY"] %>"
data-app-domain="<%= ChatChannel.urlsafe_encoded_app_domain %>"
data-honeybadger-key="<%= ApplicationConfig["HONEYBADGER_JS_API_KEY"] %>"
data-deployed-at="<%= ForemInstance.deployed_at %>"
data-latest-commit-id="<%= ForemInstance.latest_commit_id %>"
data-ga-tracking="<%= SiteConfig.ga_tracking_id %>">
<%# Repeat of stylesheets in <head> to fix rendering glitch: https://github.com/forem/forem/issues/12377 %>
<%= render "layouts/styles", qualifier: "secondary" %>
<div id="body-styles">
<style>
:root {
--accent-brand: <%= SiteConfig.primary_brand_color_hex %>;
--accent-brand-darker: <%= Color::CompareHex.new([SiteConfig.primary_brand_color_hex]).brightness(0.85) %>;
--accent-brand-lighter: <%= Color::CompareHex.new([SiteConfig.primary_brand_color_hex]).brightness(1.1) %>;
--accent-brand-a10: <%= Color::CompareHex.new([SiteConfig.primary_brand_color_hex]).opacity(0.1) %>;
}
</style>
</div>
<% if user_signed_in? %>
<%= render "layouts/user_config" %>
<% end %>
<div id="audiocontent" data-podcast="">
<%= yield(:audio) %>
</div>
<%= render "layouts/top_bar" %>
<div id="active-broadcast" class="broadcast-wrapper"></div>
<div id="message-notice"></div>
<div class="app-shell-loader">
loading...
</div>
<% end %>
<% end %>
<% if SiteConfig.payment_pointer.present? %>
<div id="base-payment-pointer" data-payment-pointer="<%= SiteConfig.payment_pointer %>"></div>
<% end %>
@ -24,4 +106,18 @@
<%= yield %>
</div>
</div>
<%= render "shell/bottom" %>
<% unless internal_navigation? %>
<% cache("footer-and-signup-modal-#{user_signed_in?}-#{ForemInstance.deployed_at}-#{SiteConfig.admin_action_taken_at&.rfc3339}", expires_in: 24.hours) do %>
<%= render "layouts/footer" %>
<%= render "layouts/signup_modal" unless user_signed_in? %>
<% end %>
<% if @shell %>
<script defer>
if (!document.title) {
document.title = document.getElementsByTagName("TITLE")[1].textContent
}
</script>
<% end %>
</body>
</html>
<% end %>

View file

@ -1,91 +1,21 @@
// Service worker file. Renders as serviceworker.js
// This is now a self-destructing service worker. See https://github.com/NekR/self-destroying-sw
<% unless Rails.env.test? || ENV["SKIP_SERVICEWORKERS"] == "true" %>
const VERSION = "1.0" // Increment to invalidate old assets.
const OFFLINE_URL = "offline.html";
const OFFLINE_NAME = OFFLINE_URL + VERSION
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
// Setting {cache: 'reload'} in the new request will ensure that the
// response isn't fulfilled from the HTTP cache; i.e., it will be from
// the network.
const offlineCache = await caches.open(OFFLINE_NAME);
await offlineCache.add(new Request(OFFLINE_URL, { cache: "reload" }));
})()
);
// Force the waiting service worker to become the active service worker.
<% unless Rails.env.test? %>
self.addEventListener('install', function(e) {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
// Enable navigation preload if it's supported.
// See https://developers.google.com/web/updates/2017/02/navigation-preload
if ("navigationPreload" in self.registration) {
await self.registration.navigationPreload.enable();
}
})()
);
// Tell the active service worker to take control of the page immediately.
self.clients.claim();
});
function includesUnsupportedPath(url) {
return [
'/%F0%9F%92%B8', // 💸 (hiring)
'/admin', // Don't run on admin dashboard.
'/new', // Don't run on editor.
'/enter', // Don't run on sign in page.
'/oauth/', // Skip oauth apps
'/onboarding', // Don't run on onboarding.
'/shop', // Don't run on Shop
'/sidekiq', // Skip for Sidekiq dashboard
'/users/auth', // Don't run on authentication.
'/users/sign_in', // Don't run on sign in page.
'/welcome', // Don't run on welcome reroutes.
].some(path => url.includes(path))
}
self.addEventListener("fetch", (event) => {
// We only want to call event.respondWith() if this is a navigation request
// for an HTML page with a few exceptions.
if (event.request.mode === "navigate" && !includesUnsupportedPath(event.request.url)) {
event.respondWith(
(async () => {
try {
// First, try to use the navigation preload response if it's supported.
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
// Always try the network first.
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
console.log("Fetch failed; returning offline page instead.", error);
const cache = await caches.open(OFFLINE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})()
);
}
// 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.
self.addEventListener('activate', function(e) {
self.registration.unregister()
.then(function() {
return self.clients.matchAll();
})
.then(function(clients) {
clients.forEach(client => {
// Check if client.navigate is supported before running it.
client.navigate && client.navigate(client.url);
})
});
});
<% end %>

View file

@ -1,26 +0,0 @@
{
"name": "<%= community_name %>",
"short_name": "<%= SiteConfig.app_domain %>",
"description": "<%= SiteConfig.community_description %>",
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#000000",
"homepage_url": "<%= app_url %>",
"icons": [{
"src": "<%= optimized_image_url(SiteConfig.logo_png, width: 192, fetch_format: "png") %>",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
}, {
"src": "<%= optimized_image_url(SiteConfig.logo_png, width: 128, fetch_format: "png") %>",
"sizes": "128x128",
"type": "image/png",
"purpose": "any maskable"
}, {
"src": "<%= optimized_image_url(SiteConfig.logo_png, width: 512, fetch_format: "png") %>",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}]
}

View file

@ -1,19 +0,0 @@
<% # TODO: This file supports removed service worker functionality. We are keeping it around until mid-March to support previously downloaded service workers. %>
<% # After that, these contents can be moved right into application.html. %>
<!-- Start Bottom Shell -->
<% unless internal_navigation? %>
<% cache("footer-and-signup-modal-#{user_signed_in?}-#{ForemInstance.deployed_at}-#{SiteConfig.admin_action_taken_at&.rfc3339}", expires_in: 24.hours) do %>
<%= render "layouts/footer" %>
<%= render "layouts/signup_modal" unless user_signed_in? %>
<% end %>
<% if @shell %>
<script defer>
if (!document.title) {
document.title = document.getElementsByTagName("TITLE")[1].textContent
}
</script>
<% end %>
</body>
</html>
<% end %>

View file

@ -1,88 +0,0 @@
<% # TODO: This file supports removed service worker functionality. We are keeping it around until mid-March to support previously downloaded service workers. %>
<% # After that, these contents can be moved right into application.html. %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<% title = yield(:title) %>
<title><%= title || community_name.to_s %></title>
<% unless internal_navigation? %>
<meta name="last-updated" content="<%= Time.current %>">
<meta name="user-signed-in" content="<%= user_signed_in? %>">
<meta name="head-cached-at" content="<%= Time.current.to_i %>">
<% if SiteConfig.payment_pointer.present? %>
<!-- Experimental web monetization payment pointer for micropayments -->
<!-- It lets readers make micropayments to websites they visit. -->
<!-- This is step 1: Get live in production to test for platform-wide payment pointer. -->
<!-- Step 2: Allow authors to set their payment pointer so they can directly monetize their content based on visitors. -->
<!-- Step 3: Enable further functionality based on what we learn from this experimentation and how the ecosystem evolves. -->
<meta name="monetization" content="<%= SiteConfig.payment_pointer %>">
<% end %>
<meta name="environment" content="<%= Rails.env %>">
<%= render "layouts/styles", qualifier: "main" %>
<% unless user_signed_in? %>
<%= javascript_packs_with_chunks_tag "base", "Search", defer: true %>
<% end %>
<%= javascript_include_tag "base", defer: true %>
<% if user_signed_in? %>
<%= javascript_packs_with_chunks_tag "base", "Search", "onboardingRedirectCheck", "contentDisplayPolicy", defer: true %>
<% end %>
<%= yield(:page_meta) %>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<%= favicon_link_tag Images::Optimizer.call(SiteConfig.favicon_url, width: 32) %>
<link rel="apple-touch-icon" href="<%= optimized_image_url(SiteConfig.logo_png, width: 180, fetch_format: "png") %>">
<link rel="apple-touch-icon" sizes="152x152" href="<%= optimized_image_url(SiteConfig.logo_png, width: 152, fetch_format: "png") %>">
<link rel="apple-touch-icon" sizes="180x180" href="<%= optimized_image_url(SiteConfig.logo_png, width: 180, fetch_format: "png") %>">
<link rel="apple-touch-icon" sizes="167x167" href="<%= optimized_image_url(SiteConfig.logo_png, width: 167, fetch_format: "png") %>">
<link href="<%= optimized_image_url(SiteConfig.logo_png, width: 192, fetch_format: "png") %>" rel="icon" sizes="192x192" />
<link href="<%= optimized_image_url(SiteConfig.logo_png, width: 128, fetch_format: "png") %>" rel="icon" sizes="128x128" />
<meta name="apple-mobile-web-app-title" content="<%= SiteConfig.app_domain %>">
<meta name="application-name" content="<%= SiteConfig.app_domain %>">
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="/manifest.json" />
<link rel="search" href="<%= URL.url("open-search.xml") %>" type="application/opensearchdescription+xml" title="<%= community_name %>" />
<meta property="forem:name" content="<%= community_name %>" />
<meta property="forem:logo" content="<%= optimized_image_url(SiteConfig.logo_png, width: 512, fetch_format: "png") %>" />
<meta property="forem:domain" content="<%= SiteConfig.app_domain %>" />
<% end %>
</head>
<% unless internal_navigation? %>
<% cache(release_adjusted_cache_key("top-html-and-config--#{user_signed_in?}")) do %>
<body
data-user-status="<%= user_logged_in_status %>"
class="<%= SiteConfig.default_font.tr("_", "-") %>-article-body default-header"
data-pusher-key="<%= ApplicationConfig["PUSHER_KEY"] %>"
data-app-domain="<%= ChatChannel.urlsafe_encoded_app_domain %>"
data-honeybadger-key="<%= ApplicationConfig["HONEYBADGER_JS_API_KEY"] %>"
data-deployed-at="<%= ForemInstance.deployed_at %>"
data-latest-commit-id="<%= ForemInstance.latest_commit_id %>"
data-ga-tracking="<%= SiteConfig.ga_tracking_id %>">
<%# Repeat of stylesheets in <head> to fix rendering glitch: https://github.com/forem/forem/issues/12377 %>
<%= render "layouts/styles", qualifier: "secondary" %>
<div id="body-styles">
<style>
:root {
--accent-brand: <%= SiteConfig.primary_brand_color_hex %>;
--accent-brand-darker: <%= Color::CompareHex.new([SiteConfig.primary_brand_color_hex]).brightness(0.85) %>;
--accent-brand-lighter: <%= Color::CompareHex.new([SiteConfig.primary_brand_color_hex]).brightness(1.1) %>;
--accent-brand-a10: <%= Color::CompareHex.new([SiteConfig.primary_brand_color_hex]).opacity(0.1) %>;
}
</style>
</div>
<% if user_signed_in? %>
<%= render "layouts/user_config" %>
<% end %>
<div id="audiocontent" data-podcast="">
<%= yield(:audio) %>
</div>
<%= render "layouts/top_bar" %>
<div id="active-broadcast" class="broadcast-wrapper"></div>
<div id="message-notice"></div>
<div class="app-shell-loader">
loading...
</div>
<% end %>
<!-- End Top Shell -->
<% end %>

View file

@ -577,9 +577,6 @@ Rails.application.routes.draw do
get "/open-search", to: "open_search#show",
constraints: { format: /xml/ }
get "/shell_top", to: "shell#top"
get "/shell_bottom", to: "shell#bottom"
get "/new", to: "articles#new"
get "/new/:template", to: "articles#new"

View file

@ -44,12 +44,3 @@ selectors more sophisticated than a simple id, class or tag name.
- [Forem PR 6380](https://github.com/forem/forem/issues/6380#issuecomment-592989438)
- [Why is getElementsByTagName() faster than querySelectorAll()?](https://humanwhocodes.com/blog/2010/09/28/why-is-getelementsbytagname-faster-that-queryselectorall/)
- [What is the difference between querySelectorAll and getElementsByTagName?](https://stackoverflow.com/a/30921553/4186181)
## Service workers in development
By default our
[service worker code](https://github.com/forem/forem/blob/master/app/views/service_worker/index.js.erb)
doesn't run in development.
If you're planning to do any work around service workers you'll need to enable
them by setting the `SKIP_SERVICEWORKERS` environment variable to `"false"`.

View file

@ -25,18 +25,18 @@ caching.
## Content precision
In some situations we may want more precise content than in others. Often when
we do not need a precise number, it offers an opportunity to either estimate
the content or bust the cache less frequently.
we do not need a precise number, it offers an opportunity to either estimate the
content or bust the cache less frequently.
### Examples
- We use the `estimated_count` for a more efficient query of registered users on
the home page. We have deemed that this is probably close enough.
- On posts and comment trees without recent comments, we do not asynchronously fetch
the absolute latest individual reaction counts for logged-out users because this
number is likely to be correct without the async call, and if it is off-by-one, we
can make the choice that it is not important that it be more precise than this.
the home page. We have deemed that this is probably close enough.
- On posts and comment trees without recent comments, we do not asynchronously
fetch the absolute latest individual reaction counts for logged-out users
because this number is likely to be correct without the async call, and if it
is off-by-one, we can make the choice that it is not important that it be more
precise than this.
## We Mostly defer scripts for usage performance improvements
@ -54,16 +54,6 @@ We use [PreactJS](/frontend/preact), a lightweight alternative to ReactJS, and
we try to reduce our bundle size with
[dynamic imports](/frontend/dynamic-imports).
## Service workers and shell architecture
We make use of serviceworkers to cache portions of the page.
Serviceworkers can be controlled in the `application` tab of Chrome.
Serviceworkers are a reverse proxy that runs in the browser in a non-blocking
thread, supported by most major browsers. You may want to disable or bypass
Serviceworkers in development while making changes to avoid having everything
cached.
## Worst technical debt
The most widespread elements of technical debt in this application reside on the

View file

@ -4,12 +4,6 @@ namespace :cache do
# Trigger cache purges for globally-cached endpoints that could have changed
[30, 180, 600].each do |n|
BustCachePathWorker.set(queue: :high_priority).perform_in(n.seconds, "/")
# TODO: Remove these "shell" endpoints, because they are for service worker functionality we no longer need.
# We are keeping these around mid-March 2021 because previously-installed service workers may still expect them.
BustCachePathWorker.set(queue: :high_priority).perform_in(n.seconds, "/shell_top")
BustCachePathWorker.set(queue: :high_priority).perform_in(n.seconds, "/shell_bottom")
BustCachePathWorker.set(queue: :high_priority).perform_in(n.seconds, "/async_info/shell_version")
BustCachePathWorker.set(queue: :high_priority).perform_in(n.seconds, "/onboarding")
end
end

View file

@ -1,360 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>You are not connected to the Internet</title>
<meta property="og:url" content="https://dev.to/offline.html" />
<meta property="og:title" content="Looks like you've lost your Internet connection" />
<meta property="og:image" content="https://thepracticaldev.s3.amazonaws.com/i/aio6nc08tpcqavej512d.png" />
<meta property="og:description" content="Maybe you could go outside and get some fresh air." />
<meta property="og:site_name" content="The DEV Community" />
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@ThePracticalDev">
<meta name="twitter:title" content="Looks like you've lost your Internet connection">
<meta name="twitter:description" content="Maybe you could go outside and get some fresh air.">
<meta name="twitter:image:src" content="https://thepracticaldev.s3.amazonaws.com/i/aio6nc08tpcqavej512d.png">
<style>
html,
body,
canvas {
user-select: none;
}
body {
justify-content: center;
align-items: center;
font-family: -apple-system, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
h2,
h3 {
font-weight: 400;
}
h2 {
font-size: 2em;
}
h3 {
font-size: 1.3em;
}
canvas {
position: fixed;
top: 0;
width: 100%;
height: 100vh;
}
path {
opacity: 50%;
}
.content {
text-align: center;
margin: calc(25px + 2vw) 2vw;
}
.paletteSelector {
z-index: 2;
margin-top: 30px;
text-align: center;
position: relative;
}
#btn-clear, .paletteSelector select {
height: 30px;
cursor: pointer;
}
.colors {
z-index: 2;
margin: 0 20px;
text-align: center;
position: relative;
min-height: 52px;
}
.color {
height: 50px;
width: 50px;
margin-right: 10px;
border-radius: 50%;
border: none;
}
.color:hover {
opacity: .7;
cursor: pointer;
}
.color:focus {
outline: 0;
}
.rainbow-logo {
border-radius: 10%;
max-width: 350px;
max-height: 350px;
}
.signature {
margin: 20px auto;
display: block;
}
.crayons-footer {
display: none;
}
@media only screen and (max-height: 700px) {
h2 {
font-size: 1.5em;
}
h3 {
font-size: 1.0em;
}
.color {
height: 30px;
width: 30px;
}
.signature {
margin-top: 10px;
margin-bottom: 10px;
}
.content {
margin-bottom: 10px;
}
}
</style>
</head>
<body>
<canvas></canvas>
<div class="content">
<svg viewBox="0 0 235 234" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="rainbow-logo"
preserveAspectRatio="xMinYMin meet">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="80K">
<polygon id="Shape" fill="#88AEDC" points="234.04 175.67 158.35 233.95 205.53 233.95 234.04 212"></polygon>
<polygon id="Shape" points="234.04 140.06 112.11 233.95 112.13 233.95 234.04 140.08"></polygon>
<polygon id="Shape" points="133.25 0.95 0.04 103.51 0.04 103.53 133.27 0.95"></polygon>
<polygon id="Shape" fill="#F58F8E" fill-rule="nonzero" points="0.04 0.95 0.04 31.11 39.21 0.95"></polygon>
<polygon id="Shape" fill="#FEE18A" fill-rule="nonzero" points="39.21 0.95 0.04 31.11 0.04 67.01 85.84 0.95"></polygon>
<polygon id="Shape" fill="#F3F095" fill-rule="nonzero" points="85.84 0.95 0.04 67.01 0.04 103.51 133.25 0.95"></polygon>
<polygon id="Shape" fill="#55C1AE" fill-rule="nonzero" points="133.27 0.95 0.04 103.53 0.04 139.12 179.49 0.95"></polygon>
<polygon id="Shape" fill="#F7B3CE" fill-rule="nonzero" points="234.04 0.95 226.67 0.95 0.04 175.45 0.04 211.38 234.04 31.2"></polygon>
<polygon id="Shape" fill="#88AEDC" fill-rule="nonzero" points="179.49 0.95 0.04 139.12 0.04 175.45 226.67 0.95"></polygon>
<polygon id="Shape" fill="#F58F8E" fill-rule="nonzero" points="234.04 31.2 0.04 211.38 0.04 233.95 18.07 233.95 234.04 67.65"></polygon>
<polygon id="Shape" fill="#FEE18A" fill-rule="nonzero" points="234.04 67.65 18.07 233.95 64.7 233.95 234.04 103.56"></polygon>
<polygon id="Shape" fill="#F3F095" fill-rule="nonzero" points="234.04 103.56 64.7 233.95 112.11 233.95 234.04 140.06"></polygon>
<polygon id="Shape" fill="#55C1AE" fill-rule="nonzero" points="234.04 140.08 112.13 233.95 158.35 233.95 234.04 175.67"></polygon>
<polygon id="Shape" fill="#F7B3CE" fill-rule="nonzero" points="234.04 212 205.53 233.95 234.04 233.95"></polygon>
</g>
</g>
</svg>
<h2> So, there's good news and bad news</h2>
<h3>Bad news: It looks like you're offline</h3>
<h3>Good news: You can draw a picture anywhere on this page while you wait to get it back!</h3>
<h4>(pick a color below && start drawing!)</h4>
</div>
<div class="colors">
</div>
<div class='paletteSelector'>
<select id="palettes"></select>
<button type="button" id="btn-clear">Clear</button>
</div>
<svg width="75px" height="75px" viewBox="0 0 266 286" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
class="signature">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard-3" transform="translate(-66.000000, -39.000000)" stroke="#F4908E" stroke-width="8">
<g id="Group" transform="translate(70.000000, 44.000000)">
<path d="M137.947881,11.0969119 C120.236214,10.7929621 127.259584,-4.63518682 110.600707,1.39809583 C98.4900537,5.78416547 5.38963223,31.9682026 1.99251552,84.34539 C0.138615017,112.929069 -0.563473858,141.639852 0.495802997,170.264134 C0.973626724,183.176113 16.3154795,204.585393 22.2134768,211.816457 C35.0843735,227.596436 61.6452079,206.201192 70.9197071,196.000824 C104.380023,159.200179 133.244668,118.47588 137.947881,67.1534449 C138.600896,60.027607 145.952931,5.41350351 126.011684,11.0969119 C112.376346,14.9830876 103.436568,65.3726702 102.950719,78.3517738 C101.357143,120.922959 86.4228272,211.00328 146.955431,222.813291 C172.852291,227.865812 169.286943,226.931981 192,226.931981"
id="Path-2"></path>
<path d="M80,277.169785 C120.408772,252.192233 121.434199,237.685468 133.304963,222.650446 C151.591469,199.489509 173.572805,172.509583 233.39048,84.1055622 C240.794147,73.1637472 242.286005,30.9559106 238.678367,28.2608272 C236.630233,26.7307705 219.030597,13.826187 211.887732,18.9306581 C202.734845,25.4715418 203.241886,57.600262 203.223682,60.0372493 C203.010014,88.6406571 203.914879,117.248834 205.052575,145.830265 C206.126657,172.81357 211.080162,198.940616 208.995298,226.072378 C206.84232,254.090552 155.519583,271.514023 133.304963,268.076421 C126.954422,267.093706 93.5574761,243.825981 102.987596,240.93683 C113.776095,237.631506 146.615548,245.587641 160.390474,240.93683 C174.165399,236.286019 182.361602,234.057591 192.833448,229.336442 C203.454817,224.547881 249.685075,198.800486 259.8975,193.187538"
id="Path-3"></path>
</g>
</g>
</g>
</svg>
<script type="text/javascript">
const palettes = {
default: {
name: 'Default Palette',
colors: ['#F4908E', '#F2F097', '#88B0DC', '#F7B5D1', '#53C4AF', '#FDE38C']
},
// Color-deficient accessible palettes: https://owi.usgs.gov/blog/tolcolors/
paulTol: {
name: 'Paul Tol Palette',
colors: ['#B997C6', '#824D99', '#4E79C4', '#57A2AC', '#7EB875', '#D0B440', '#E67F33']
},
// Add more palette options below and to the "select" dropdown.
}
let activePalette = 'default' // match key inside the palettes variable
// Globally DOM nodes + variables
const paletteSelector = document.querySelector('.paletteSelector')
const colorSelector = document.querySelector('.colors')
const canvas = document.querySelector('canvas')
const context = canvas.getContext('2d')
const colorDiv = document.querySelector('.colors')
let dx = 1, dy = 1;
// handler for color input buttons
function handleButtonClick(event) {
const buttonStyle = event.target.style
context.strokeStyle = buttonStyle.backgroundColor
}
// Main drawing function
function renderColorButtons(colors) {
// First, remove existing event listeners to prevent memory leak
colorDiv.querySelectorAll('button').forEach((button) => {
button.removeEventListener('click', handleButtonClick)
})
// Then, remove existing buttons
colorDiv.innerHTML = ''
// Lastly, add new a new button for each color in the passed in palette
colors.forEach(color => {
const button = document.createElement('button')
button.classList.add('color')
button.style.backgroundColor = color
colorDiv.appendChild(button)
button.addEventListener('click', handleButtonClick)
})
}
// Helper for modifying canvas context
const setContextSettings = (strokeColor) => {
context.strokeStyle = strokeColor
context.lineJoin = 'round'
context.lineWidth = 5
}
const setCanvasSize = () => {
// set dimensions on the canvas
canvas.setAttribute('width', window.innerWidth)
canvas.setAttribute('height', window.innerHeight)
}
const setResizeFactors = () => {
// set the x and y resizinf factor for cursor position
dx = canvas.width / window.innerWidth
dy = canvas.height / window.innerHeight
}
const handleResize = () => {
setCanvasSize()
setResizeFactors();
}
window.addEventListener('resize', handleResize)
// Main function to render new DOM whenever activePalette changes
const render = (palette) => {
renderColorButtons(palette)
setContextSettings(palette[0])
setCanvasSize()
}
// User Input Event Handlers
//----------------------------------------
let firstX, firstY, secondX, secondY, paint
function getCoordinates(event) {
// check to see if mobile or desktop
if (['mousedown', 'mousemove'].includes(event.type)) {
// click events
return [event.pageX * dx, event.pageY * dy]
} else {
// touch coordinates
return [event.touches[0].pageX * dx, event.touches[0].pageY * dy]
}
}
function addClick(event, canvas) {
let [x, y] = getCoordinates(event)
secondX = firstX
secondY = firstY
firstX = x
firstY = y
}
function draw() {
context.beginPath()
context.moveTo(secondX, secondY)
context.lineTo(firstX, firstY)
context.closePath()
context.stroke()
}
function startPaint(event) {
colorSelector.style.pointerEvents = 'none'
paletteSelector.style.pointerEvents = 'none'
addClick(event, this)
paint = true
}
canvas.addEventListener('mousedown', startPaint)
canvas.addEventListener('touchstart', startPaint)
const exit = _ => {
colorSelector.style.pointerEvents = 'all'
paletteSelector.style.pointerEvents = 'all'
paint = false
firstX = null
firstY = null
}
canvas.addEventListener('mouseup', exit)
canvas.addEventListener('mouseleave', exit)
canvas.addEventListener('touchend', exit)
function endPaint(event) {
if (paint) {
addClick(event, this)
draw()
}
}
canvas.addEventListener('mousemove', endPaint)
canvas.addEventListener('touchmove', endPaint)
// generate options for select tag
let selectTag = document.getElementById('palettes');
Object.keys(palettes).forEach((key) => {
// create tag
let option = document.createElement('option');
option.value = key;
option.textContent = palettes[key].name;
// insert to select
selectTag.appendChild(option);
});
paletteSelector.onchange = (event) => {
activePalette = event.target.value
render(palettes[activePalette].colors)
}
document.getElementById('btn-clear').addEventListener('click', (e) => {
context.clearRect(0, 0, canvas.width, canvas.height);
});
// Initialization when page is ready to load, runs once
(function() {
render(palettes[activePalette].colors)
})()
</script>
</body>
</html>

View file

@ -139,20 +139,6 @@ RSpec.describe ApplicationHelper, type: :helper do
end
end
describe "#sanitized_referer" do
it "returns a safe referrer unmodified" do
expect(sanitized_referer("/some/path")).to eq("/some/path")
end
it "returns nil if the referer is the service worker" do
expect(sanitized_referer("/serviceworker.js")).to be nil
end
it "returns nil if the referer is empty" do
expect(sanitized_referer("")).to be nil
end
end
describe "#collection_link" do
let(:collection) { create(:collection, :with_articles) }

View file

@ -246,20 +246,6 @@ RSpec.describe "Pages", type: :request do
get "/report-abuse", headers: { referer: url }
expect(response.body).to include(url)
end
it "does not prefill if the provide url is /serviceworker.js" do
url = "https://dev.to/serviceworker.js"
get "/report-abuse", headers: { referer: url }
expect(response.body).not_to include(url)
end
end
context "when provided the params" do
it "prefills with the provided param url" do
url = "https://dev.to/serviceworker.js"
get "/report-abuse", params: { url: url }
expect(response.body).to include(url)
end
end
end
end

View file

@ -1,37 +0,0 @@
require "rails_helper"
RSpec.describe "ServiceWorker", type: :request do
describe "GET /serviceworker.js" do
it "renders file with proper text" do
get "/serviceworker.js"
expect(response.body).to include("Service worker file")
end
it "renders javascript file" do
get "/serviceworker.js"
expect(response.header["Content-Type"]).to include("text/javascript")
end
it "sends a surrogate key (for Fastly's user)" do
get "/serviceworker.js"
expect(response.header["Surrogate-Key"]).to include("serviceworker-js")
end
end
describe "GET /manifest.json" do
it "renders file with proper text" do
get "/manifest.json"
expect(response.body).to include("\"name\": \"#{SiteConfig.community_name}\"")
end
it "renders json file" do
get "/manifest.json"
expect(response.header["Content-Type"]).to include("application/json")
end
it "sends a surrogate key (for Fastly's user)" do
get "/manifest.json"
expect(response.header["Surrogate-Key"]).to include("manifest-json")
end
end
end

View file

@ -1,39 +0,0 @@
require "rails_helper"
RSpec.describe "Shells", type: :request do
describe "GET /shell_top" do
it "renders file with proper text" do
get "/shell_top"
expect(response.body).to include("user-signed-in")
end
it "sends a surrogate key (for Fastly's user)" do
get "/shell_top"
expect(response.header["Surrogate-Key"]).to include("shell-top")
end
it "renders normal response even if site config is private" do
allow(SiteConfig).to receive(:public).and_return(false)
get "/shell_top"
expect(response.body).to include("user-signed-in")
end
end
describe "GET /shell_bottom" do
it "renders file with proper text" do
get "/shell_bottom"
expect(response.body).to include("footer-container")
end
it "sends a surrogate key (for Fastly's user)" do
get "/shell_bottom"
expect(response.header["Surrogate-Key"]).to include("shell-bottom")
end
it "renders normal response even if site config is private" do
allow(SiteConfig).to receive(:public).and_return(false)
get "/shell_bottom"
expect(response.body).to include("footer-container")
end
end
end