diff --git a/app/assets/stylesheets/scaffolds.scss b/app/assets/stylesheets/scaffolds.scss index b307f6316..bfc3873bc 100644 --- a/app/assets/stylesheets/scaffolds.scss +++ b/app/assets/stylesheets/scaffolds.scss @@ -267,6 +267,7 @@ body.trusted-status-true .trusted-visible-block { color: white; display: inline-block; margin-bottom: 0px; + border-radius: 3px; } h2 { font-size: 22px; @@ -281,4 +282,34 @@ body.trusted-status-true .trusted-visible-block { opacity: 0.9; } } + form button { + background: $red; + color: white; + font-size: 1.5em; + border: 0px; + border-radius: 3px; + padding: 6px 12px; + } +} + +.app-shell-loader { + position: absolute; + top: 200px; + text-align: center; + left: 0; + right: 0; + opacity: 0; + animation: loading-fadein 1.5s; + animation-delay: 0.6s; + font-size: 1.2em; + z-index: -1; +} + +.base-background-color { + @include themeable(background, theme-background, $lightest-gray); +} + +@keyframes loading-fadein { + from { opacity: 0; } + to { opacity: 0.6; } } diff --git a/app/assets/stylesheets/settings.scss b/app/assets/stylesheets/settings.scss index 8c10b389f..587df88c7 100755 --- a/app/assets/stylesheets/settings.scss +++ b/app/assets/stylesheets/settings.scss @@ -670,6 +670,13 @@ .signout_confirm-wrapper { padding: calc(15% + 50px) 2% 30%; text-align: center; + form button { + font-size: 1.5em; + padding: 8px 15px; + border: 0px; + border-radius: 3px; + margin-top: 15px; + } } .org-widget-preview .primary-sticky-nav { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5140e2af6..f0eccec5f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -36,7 +36,7 @@ class ApplicationController < ActionController::Base def after_sign_in_path_for(resource) return "/onboarding?referrer=#{request.env['omniauth.origin'] || 'none'}" unless current_user.saw_onboarding - request.env["omniauth.origin"] || stored_location_for(resource) || "/dashboard" + (request.env["omniauth.origin"] || stored_location_for(resource) || "/dashboard") + "?signin=true" # This signin=true param is used by frontend end def raise_banned diff --git a/app/controllers/async_info_controller.rb b/app/controllers/async_info_controller.rb index 72a38157e..7ee5403b9 100644 --- a/app/controllers/async_info_controller.rb +++ b/app/controllers/async_info_controller.rb @@ -1,6 +1,7 @@ class AsyncInfoController < ApplicationController include Devise::Controllers::Rememberable # No pundit policy. All actions are unrestricted. + before_action :set_cache_control_headers, only: %i[shell_version] def base_data flash.discard(:notice) @@ -29,6 +30,13 @@ class AsyncInfoController < ApplicationController end end + def shell_version + set_surrogate_key_header "shell-version-endpoint" + # shell_version will change on every deploy. *Technically* could be only on changes to assets and shell, but this is more fool-proof. + shell_version = ApplicationConfig["HEROKU_SLUG_COMMIT"] + render json: { version: Rails.env.production? ? shell_version : rand(1000) }.to_json + end + def user_data Rails.cache.fetch(user_cache_key, expires_in: 15.minutes) do { diff --git a/app/controllers/shell_controller.rb b/app/controllers/shell_controller.rb new file mode 100644 index 000000000..ef2bd2ce2 --- /dev/null +++ b/app/controllers/shell_controller.rb @@ -0,0 +1,13 @@ +class ShellController < ApplicationController + layout false + + def top + @shell = true + render partial: "top" + end + + def bottom + @shell = true + render partial: "bottom" + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 9dd1887d9..9def0832c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -15,38 +15,6 @@ module ApplicationHelper end end - def core_pages? - %w[ - articles - podcast_episodes - events - tags - registrations - users - pages - chat_channels - dashboards - moderations - videos - badges - stories - comments - notifications - reading_list_items - html_variants - classified_listings - credits - partnerships - pro_memberships - ].include?(controller_name) - end - - def render_js? - article_pages = controller_name == "articles" && %(index show).include?(controller.action_name) - pulses_pages = controller_name == "pulses" - !(article_pages || pulses_pages) - end - def title(page_title) derived_title = if page_title.include?(ApplicationConfig["COMMUNITY_NAME"]) page_title diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index ac0ea020b..9a0b1d7f2 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -88,6 +88,9 @@ export default class Chat extends Component { channel => `open-channel-${channel.chat_channel_id}`, ); setupObserver(this.observerCallback); + if (!window.currentUser) { + window.currentUser = JSON.parse(document.body.dataset.user) + } this.subscribePusher( `private-message-notifications-${window.currentUser.id}`, ); diff --git a/app/javascript/contentDisplayPolicy/hideBlockedContent.js b/app/javascript/contentDisplayPolicy/hideBlockedContent.js index 05558f6b7..45133e683 100644 --- a/app/javascript/contentDisplayPolicy/hideBlockedContent.js +++ b/app/javascript/contentDisplayPolicy/hideBlockedContent.js @@ -2,8 +2,8 @@ export default function hideBlockedContent() { const contentUserElements = Array.from( document.querySelectorAll('div[data-content-user-id]'), ); - /* eslint-disable-next-line no-undef */ - const blockedUserIds = userData().blocked_user_ids; + const user = userData(); //global var + const blockedUserIds = user ? user.blocked_user_ids : []; const divsToHide = contentUserElements.filter(div => { const { contentUserId } = div.dataset; diff --git a/app/views/articles/delete_confirm.html.erb b/app/views/articles/delete_confirm.html.erb index 26cd73dcf..a93d2ec10 100644 --- a/app/views/articles/delete_confirm.html.erb +++ b/app/views/articles/delete_confirm.html.erb @@ -1,5 +1,3 @@ -<%= javascript_include_tag "application" %> -

<%= @article.title %>

Are you sure you want to delete this article?

@@ -11,8 +9,7 @@ instead? - -

- DELETE -

+ <%= form_tag "/articles/#{@article.id}", method: :delete do %> + + <% end %>
diff --git a/app/views/articles/index.html.erb b/app/views/articles/index.html.erb index 2d3a44bb4..7df6e5fab 100644 --- a/app/views/articles/index.html.erb +++ b/app/views/articles/index.html.erb @@ -25,7 +25,6 @@ <% end %> <%= javascript_pack_tag "homePage", defer: true %> -<% params.delete(:i) %> <% cache("main-stories-index-#{params}-#{user_signed_in?}", expires_in: 3.minutes) do %>
<%= render "articles/tags/meta" %> <% end %> -<% params.delete(:i) %> <% expiry_minutes = params[:timeframe].blank? || params[:timeframe] == "latest" ? 4 : 20 %> <% flag = true %> <% cache("tag-stories-index-#{params}-#{flag}-#{@tag_model.updated_at}", expires_in: expiry_minutes.minutes) do %> diff --git a/app/views/classified_listings/delete_confirm.html.erb b/app/views/classified_listings/delete_confirm.html.erb index bca59c60a..14cc6a4cb 100644 --- a/app/views/classified_listings/delete_confirm.html.erb +++ b/app/views/classified_listings/delete_confirm.html.erb @@ -12,7 +12,7 @@ instead? -

- DELETE -

+ <%= form_tag "/listings/#{@classified_listing.id}", method: :delete do %> + + <% end %>
diff --git a/app/views/comments/delete_confirm.html.erb b/app/views/comments/delete_confirm.html.erb index e7affda88..89b85bfd2 100644 --- a/app/views/comments/delete_confirm.html.erb +++ b/app/views/comments/delete_confirm.html.erb @@ -1,5 +1,3 @@ -<%= javascript_include_tag "application" %> -

<%= truncate(strip_tags(@comment.processed_html.html_safe), length: 80) %>

@@ -7,8 +5,8 @@

You cannot undo this action, perhaps you just want to EDIT instead?

-

- DELETE -

+ <%= form_tag "/comments/#{@comment.id}", method: :delete do %> + + <% end %>
diff --git a/app/views/layouts/_nav_menu.html.erb b/app/views/layouts/_nav_menu.html.erb index d96941c66..52a59677e 100644 --- a/app/views/layouts/_nav_menu.html.erb +++ b/app/views/layouts/_nav_menu.html.erb @@ -30,7 +30,7 @@ Key Links - +
Sign Out
diff --git a/app/views/layouts/_styles.html.erb b/app/views/layouts/_styles.html.erb index ab0d24085..333330507 100644 --- a/app/views/layouts/_styles.html.erb +++ b/app/views/layouts/_styles.html.erb @@ -1,5 +1,10 @@ -<% cache "base_inline_styles_#{@story_show}_#{@article_index}_#{@home_page}_#{@article_show}_#{view_class}_#{@notifications_index}_#{core_pages?}_#{@tags_index}_#{@reading_list_items_index}_#{@history_index}_#{ApplicationConfig['HEROKU_SLUG_COMMIT']}_#{user_signed_in?}", expires_in: 8.hours do %> - <% if @story_show %> +<% cache "base_inline_styles_#{@story_show}_#{@article_index}_#{@home_page}_#{@article_show}_#{view_class}_#{@notifications_index}_#{@tags_index}_#{@reading_list_items_index}_#{@history_index}_#{ApplicationConfig['HEROKU_SLUG_COMMIT']}_#{user_signed_in?}_#{@shell}", expires_in: 8.hours do %> + <% if @shell %> + + <% elsif @story_show %> - <% elsif core_pages? %> - <%= stylesheet_link_tag "minimal", media: "all" %> <% else %> - <%= stylesheet_link_tag "application", media: "all" %> + <%= stylesheet_link_tag "minimal", media: "all" %> <% end %> <% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 6df2a7b79..e4062912b 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,110 +1,13 @@ - - - - - <% title = yield(:title) %> - <%= title || "#{ApplicationConfig['COMMUNITY_NAME']} Community" %> - <% if internal_navigation? %> - - <% else %> - - - "> - - - <%= render "layouts/styles" %> - - <%= javascript_pack_tag "manifest", defer: true %> - <%= javascript_pack_tag "vendor", defer: true %> - <%= javascript_pack_tag "Search", defer: true %> - <% if core_pages? %> - <%= javascript_include_tag "base", defer: true %> - <% if user_signed_in? %> - <%= javascript_pack_tag "onboardingRedirectCheck", defer: true %> - <%= javascript_pack_tag "contentDisplayPolicy", defer: true %> - <% end %> - <% end %> - <% if !core_pages? %> - <%= stylesheet_link_tag "application", media: "all" %> - <% if render_js? %> - - <%= javascript_include_tag "application" %> - <% end %> - <% end %> - <% if view_class == "users users-signout_confirm" %> - <%= javascript_include_tag "application" %> - <% end %> - <%= yield(:page_meta) %> - - <%= favicon_link_tag ApplicationConfig["FAVICON_URL"] %> - "> - "> - "> - "> - " rel="icon" sizes="192x192" /> - " rel="icon" sizes="128x128" /> - - - - - <% if view_class.include? "users-signout_confirm" %> - <%= csrf_meta_tags %> - <% end %> - - - <% end %> - - "> -
- <% if user_signed_in? && !internal_navigation? %> - <%= render "layouts/user_config" %> - <% end %> - <% unless internal_navigation? %> -
- <%= yield(:audio) %> -
- <% end %> - <% cache("application-top-bar--#{user_signed_in?}--#{internal_navigation?}", expires_in: 100.hours) do %> - <%= render "layouts/top_bar" unless internal_navigation? %> - <% end %> -
-
- <% if flash[:global_notice] %> -
- <%= flash[:global_notice] %> -
- <% end %> -
- <%= yield %> -
+<%= render "shell/top" %> + +
+ <% if flash[:global_notice] %> +
+ <%= flash[:global_notice] %>
- <% unless internal_navigation? %> - <% cache("footer-and-signup-modal--#{user_signed_in?}--#{ApplicationConfig['HEROKU_SLUG_COMMIT']}", expires_in: 24.hours) do %> - <%= render "layouts/footer" %> - <%= render "layouts/signup_modal" unless user_signed_in? %> - <% end %> -
- <%= image_tag("twitter-logo.svg", class: "icon-img", style: "display:none", alt: "twitter logo") %> - <%= image_tag("github-logo.svg", class: "icon-img", style: "display:none", alt: "github logo") %> - <% end %> - - + <% end %> +
+ <%= yield %> +
+
+<%= render "shell/bottom" %> \ No newline at end of file diff --git a/app/views/service_worker/index.js.erb b/app/views/service_worker/index.js.erb index b4eddeac2..7540ea4d3 100644 --- a/app/views/service_worker/index.js.erb +++ b/app/views/service_worker/index.js.erb @@ -1,144 +1,136 @@ -var CACHE_VERSION = 'v2.7.1'; -var CACHE_NAME = CACHE_VERSION + ':sw-cache::'; -var REQUESTS_LIMIT = 40; +// Serviceworkers file. This code gets installed in users browsers and runs code before the request is made. +<% unless Rails.env.test? %> + const staticCacheName = 'static-1'; + const expectedCaches = [ + staticCacheName + ]; -function onInstall(event) { - event.waitUntil( - caches.open(CACHE_NAME).then(function prefill(cache) { - return cache.addAll([ - '<%= asset_path "base.js" %>', - '<%= asset_path "minimal.css" %>', - '/offline.html', - '<%= asset_path "devword.png" %>', - '<%= asset_path "wires.png" %>', - '<%= asset_path "comments-bubble.png" %>', - '<%= asset_path "reactions-stack.png" %>', - '<%= asset_path "emoji/emoji-one-heart.png" %>', - '<%= asset_path "emoji/emoji-one-unicorn.png" %>', - '<%= asset_path "emoji/emoji-one-bookmark.png" %>', - '<%= asset_path "emoji/apple-fire.png" %>', - ]).then(function () { - console.log("WORKER: Install completed"); - }); - }) - ); -} + self.addEventListener('install', event => { + self.skipWaiting(); -function onActivate(event) { - console.log('[Serviceworker]', "Activating!", event); - event.waitUntil( - caches.keys().then(function(cacheNames) { - return Promise.all( - cacheNames.filter(function(cacheName) { - // Return true if you want to remove this cache, - return cacheName.indexOf(CACHE_NAME) !== 0; - }).map(function(cacheName) { - return caches.delete(cacheName); + // Populate initial serviceworker cache. + event.waitUntil( + caches.open(staticCacheName) + .then(cache => cache.addAll([ + "/shell_top", // head, top bar, inline styles + "/shell_bottom", // footer + "/async_info/shell_version", // For comparing changes in the shell. Should be incremented with style changes. + "/404.html", // Not found page + "/500.html", // Error page + "/offline.html" //Offline page + ])) + ); + }); + + // remove caches that aren't in expectedCaches + self.addEventListener('activate', event => { + event.waitUntil( + caches.keys().then(keys => Promise.all( + keys.map(key => { + if (!expectedCaches.includes(key)) return caches.delete(key); }) - ); - }) - ); -} + )) + ); + }); -function onFetch(event) { - if ( requestBlackListed(event.request) ) { return; } - event.respondWith( - caches.match(event.request).then(function (cached) { - if (cached && shouldReturnStraightFromCache(event.request.url)) { - return cached; - } - return fetch(event.request) - .then(fetchedFromNetwork) - .catch(function fallback(err) { - console.log(err) - return cached || caches.match('/offline.html'); - }) - } - ) - ); - - - function requestBlackListed(request){ - var url = new URL(request.url); - var test = (event.request.method !== 'GET' || - event.request.url.match(/\/notifications(\?|\/)/) || - url.host.indexOf("facebook") > -1 || - url.host.indexOf("google") > -1 ) - return test - } - - function shouldReturnStraightFromCache(url) { - return (url === "<%= asset_path "base.js" %>" || - url === "<%= asset_path "minimal.css" %>" || - url.indexOf(".self-") > -1 || - (url.indexOf("search?") > -1 && url.indexOf("&i=i") > -1) || - url.indexOf("freetls.fastly.net") > -1) - } - - function fetchedFromNetwork(response) { - var cacheCopy = response.clone(); - caches - .open(CACHE_NAME + 'pages') - .then(function add(cache) { - if ( shouldReturnStraightFromCache(event.request.url) ) { - cache.put(event.request, cacheCopy); + // Create a composed streamed webpage with shell and core content + function createPageStream(request) { + const stream = new ReadableStream({ + start(controller) { + if (!caches.match('/shell_top') || !caches.match('/shell_top')) { //return if shell isn't cached. + return } - garbageCollect(cache); - }) - return response; - } - function garbageCollect(cache) { - cache.keys().then(function count(keys) { - if (keys.length > REQUESTS_LIMIT) { - cache.delete(keys[0]); - cache.delete(keys[1]); - cache.delete(keys[2]); - cache.delete(keys[3]); - cache.delete(keys[4]); - cache.delete(keys[5]); - cache.delete(keys[6]); - cache.delete(keys[7]); - cache.delete(keys[8]); - cache.delete(keys[9]); - cache.delete(keys[10]); + // the body url is the request url plus 'include' + const url = new URL(request.url); + url.searchParams.set('i', 'i'); // Adds ?i=i or &i=i, which is our indicator for "internal" partial page + const startFetch = caches.match('/shell_top'); + const endFetch = caches.match('/shell_bottom'); + const middleFetch = fetch(url).then(response => { + if (!response.ok && response.status === 404) { + return caches.match('/404.html'); + } + if (!response.ok && response.status != 404) { + return caches.match('/500.html'); + } + return response; + }).catch(err => caches.match('/offline.html')); + + function pushStream(stream) { + const reader = stream.getReader(); + return reader.read().then(function process(result) { + if (result.done) return; + controller.enqueue(result.value); + return reader.read().then(process); + }); + } + startFetch + .then(response => pushStream(response.body)) + .then(() => middleFetch) + .then(response => pushStream(response.body)) + .then(() => endFetch) + .then(response => pushStream(response.body)) + .then(() => controller.close()); } - }) + }); + + return new Response(stream, { + headers: {'Content-Type': 'text/html; charset=utf-8'} + }); } - function unableToResolve () { - console.log('WORKER: fetch request failed in both cache and network.'); - return caches.match('/offline.html') - } -}; + self.addEventListener('fetch', event => { + const url = new URL(event.request.url); + if (url.origin === location.origin) { + // + if (event.clientId === "" && // Not fetched via AJAX after page load. + event.request.method == "GET" && // Don't fetch on POST, DELETE, etc. + !event.request.referrer.includes('/signout_confirm') && // If this is the referrer, we instead want to flush. + !url.href.includes('i=i') && // Parameter representing "internal" navigation. + !url.href.includes('?signin') && // Don't run on sign in. + !url.href.includes('.') && // Don't run on sign in. + !url.href.includes('/onboarding') && // Don't run on onboarding. + !url.href.includes('/api/') && // Don't run on API endpoints. + !url.href.includes('/users/auth') && // Don't run on authentication. + !url.href.includes('/shell_') && // Don't fetch for shell. + !url.href.includes('/admin') && // Don't fetch for administrate dashboard. + !url.href.includes('/internal') && // Don't fetch for internal dashboard. + caches.match('/shell_top') && // Ensure shell_top is in the cache. + caches.match('/shell_bottom')) { // Ensure shell_bottom is in the cache. + event.respondWith(createPageStream(event.request)); // Respond with the stream -function onPush(event) { - var title = 'DEV Connect 👋 '; - var body = event.data.text(); - var icon = '<%= image_path "devlogo-pwa-128.png" %>'; - var tag = 'default-tag' + body; - event.waitUntil( - self.registration.showNotification(title, { - body: body, - icon: icon, - tag: tag, - data: { - url: 'https://dev.to/connect' + // Ping version endpoint to see if we should fetch new shell. + if (!caches.match('/async_info/shell_version')) { // Check if we have a cached shell version + caches.open(staticCacheName) + .then(cache => cache.addAll([ + "/async_info/shell_version", + ])) + return + } + fetch('/async_info/shell_version').then(response => response.json()).then(json => { + caches.match('/async_info/shell_version').then(cachedResponse => cachedResponse.json()).then(cacheJson => { + if (cacheJson['version'] != json['version']) { + caches.open(staticCacheName) + .then(cache => cache.addAll([ + "/shell_top", + "/shell_bottom", + "/async_info/shell_version" + ])) + } + }) + }); + return; } - }) - ); -}; -function onNotificationClick(event) { - if (Notification.prototype.hasOwnProperty('data')) { - var url = event.notification.data.url; - event.waitUntil(clients.openWindow(url)); - } -} - - -self.addEventListener('install', onInstall); -self.addEventListener('activate', onActivate); -self.addEventListener('fetch', onFetch); -self.addEventListener('push', onPush); -self.addEventListener('notificationclick', onNotificationClick) \ No newline at end of file + // Fetch new shell upon events that signify change in session. + if (event.clientId === "" && + (event.request.referrer.includes('/signout_confirm') || url.href.includes('?signin') || url.href.includes('/onboarding'))) { + caches.open(staticCacheName) + .then(cache => cache.addAll([ + "/shell_top", + "/shell_bottom", + ])) + } + } + }); +<% end %> \ No newline at end of file diff --git a/app/views/shell/_bottom.html.erb b/app/views/shell/_bottom.html.erb new file mode 100644 index 000000000..a9d88df17 --- /dev/null +++ b/app/views/shell/_bottom.html.erb @@ -0,0 +1,19 @@ + +<% unless internal_navigation? %> + <% cache("footer-and-signup-modal--#{user_signed_in?}--#{ApplicationConfig['HEROKU_SLUG_COMMIT']}", expires_in: 24.hours) do %> + <%= render "layouts/footer" %> + <%= render "layouts/signup_modal" unless user_signed_in? %> + <% end %> +
+ <%= image_tag("twitter-logo.svg", class: "icon-img", style: "display:none", alt: "twitter logo") %> + <%= image_tag("github-logo.svg", class: "icon-img", style: "display:none", alt: "github logo") %> + <% if @shell %> + + <% end %> + + +<% end %> diff --git a/app/views/shell/_top.html.erb b/app/views/shell/_top.html.erb new file mode 100644 index 000000000..174153d8b --- /dev/null +++ b/app/views/shell/_top.html.erb @@ -0,0 +1,75 @@ + + + + + <% title = yield(:title) %> + <%= title || "#{ApplicationConfig['COMMUNITY_NAME']} Community" %> + <% unless internal_navigation? %> + + + "> + + + <%= render "layouts/styles" %> + + <%= javascript_pack_tag "manifest", defer: true %> + <%= javascript_pack_tag "vendor", defer: true %> + <%= javascript_pack_tag "Search", defer: true %> + <%= javascript_include_tag "base", defer: true %> + <% if user_signed_in? %> + <%= javascript_pack_tag "onboardingRedirectCheck", defer: true %> + <%= javascript_pack_tag "contentDisplayPolicy", defer: true %> + <% end %> + <%= yield(:page_meta) %> + + <%= favicon_link_tag ApplicationConfig["FAVICON_URL"] %> + "> + "> + "> + "> + " rel="icon" sizes="192x192" /> + " rel="icon" sizes="128x128" /> + + + + + <% if view_class.include? "users-signout_confirm" %> + <%= csrf_meta_tags %> + <% end %> + + + <% end %> + + <% unless internal_navigation? %> + "> +
+ <% if user_signed_in? %> + <%= render "layouts/user_config" %> + <% end %> +
+ <%= yield(:audio) %> +
+ <% cache("application-top-bar--#{user_signed_in?}", expires_in: 100.hours) do %> + <%= render "layouts/top_bar" %> + <% end %> +
+
+ loading... +
+ + <% end %> \ No newline at end of file diff --git a/app/views/users/signout_confirm.html.erb b/app/views/users/signout_confirm.html.erb index 8c93fdd73..38746d5a3 100644 --- a/app/views/users/signout_confirm.html.erb +++ b/app/views/users/signout_confirm.html.erb @@ -1,9 +1,9 @@ <% title "Confirm Signout - DEV" %> -

Are you sure you want to sign out?

- -

<%= link_to "Yes. Sign out.", destroy_user_session_path, method: :delete, data: { "no-instant" => "" } %>

+ <%= form_tag destroy_user_session_path, method: :delete do %> + + <% end %>
diff --git a/config/environments/development.rb b/config/environments/development.rb index 9acf2cb5c..bb425cf7c 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -102,8 +102,8 @@ Rails.application.configure do config.logger = ActiveSupport::TaggedLogging.new(logger) config.after_initialize do - Bullet.enable = true - Bullet.console = true + Bullet.enable = false + Bullet.console = false Bullet.rails_logger = true end end diff --git a/config/environments/production.rb b/config/environments/production.rb index f9d28d591..b298150d8 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -28,7 +28,7 @@ Rails.application.configure do # Apache or NGINX already handles this. config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? config.public_file_server.headers = { - "Cache-Control" => "public, s-maxage=2592000, max-age=86400" + "Cache-Control" => "public, s-maxage=#{30.days.to_i}, max-age=#{30.days.to_i}" } # Compress JavaScripts and CSS. diff --git a/config/initializers/fastly.rb b/config/initializers/fastly.rb index 6573572f7..1b6dc5dd5 100644 --- a/config/initializers/fastly.rb +++ b/config/initializers/fastly.rb @@ -1,6 +1,6 @@ FastlyRails.configure do |c| c.api_key = ApplicationConfig["FASTLY_API_KEY"] # Fastly api key, required - c.max_age = 30.days.to_i # time in seconds, optional, defaults to 2592000 (30 days) + c.max_age = 1.day.to_i # time in seconds, optional, defaults to 2592000 (30 days) c.service_id = ApplicationConfig["FASTLY_SERVICE_ID"] # Fastly service you will be using, required c.stale_if_error = 26_400 c.purging_enabled = Rails.env.production? # No need to configure a client locally (since 0.4.0) diff --git a/config/initializers/initialization_cachebust.rb b/config/initializers/initialization_cachebust.rb new file mode 100644 index 000000000..1dc0cc18c --- /dev/null +++ b/config/initializers/initialization_cachebust.rb @@ -0,0 +1,4 @@ +# Trigger cache purges for globally-cached endpoints that could have changed +CacheBuster.bust("/shell_top") +CacheBuster.bust("/shell_bottom") +CacheBuster.bust("/async_info/shell_version") diff --git a/config/routes.rb b/config/routes.rb index 66cf3c603..62fb0b6f0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -235,7 +235,8 @@ Rails.application.routes.draw do get "/social_previews/comment/:id" => "social_previews#comment", :as => :comment_social_preview get "/async_info/base_data", controller: "async_info#base_data", defaults: { format: :json } - + get "/async_info/shell_version", controller: "async_info#shell_version", defaults: { format: :json } + get "/future", to: redirect("devteam/the-future-of-dev-160n") # Settings @@ -338,6 +339,10 @@ Rails.application.routes.draw do get "/serviceworker" => "service_worker#index" get "/manifest" => "service_worker#manifest" + get "/shell_top" => "shell#top" + get "/shell_bottom" => "shell#bottom" + + get "/new" => "articles#new" get "/new/:template" => "articles#new" diff --git a/docs/technical-overview/architecture.md b/docs/technical-overview/architecture.md index 25879b97a..6219e7daf 100644 --- a/docs/technical-overview/architecture.md +++ b/docs/technical-overview/architecture.md @@ -35,6 +35,20 @@ rule, you should avoid relying on JavaScript for layout when working on DEV. We use [PreactJS][preact], a lightweight alternative to ReactJS, and we try to reduce our bundle size with [dynamic imports][dynamic_imports]. +## Service workers and shell architecture + +We make use of serviceworkers to cache portions of the page. + +We cache styles here as well as script fingerprinting, so we should increment +the number in `/async_info/shell_version` any time we change core CSS or +JavaScript. + +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 @@ -63,16 +77,16 @@ shared among all users. ## Inter-page navigation -DEV uses a variation of "instant click," which swaps out page content instead -of making full-page requests. This approach is similar to the one used by the -Rails gem `Turbolinks`, but our approach is more lightweight. The library is -modified to work specifically with this Rails app and does not swap out reused -elements like the navigation bar or the footer. The code for this functionality -is viewable in `app/assets/javascripb/base.js.erb`. +DEV uses a variation of "instant click," which swaps out page content instead of +making full-page requests. This approach is similar to the one used by the Rails +gem `Turbolinks`, but our approach is more lightweight. The library is modified +to work specifically with this Rails app and does not swap out reused elements +like the navigation bar or the footer. The code for this functionality is +viewable in `app/assets/javascripb/base.js.erb`. There are a few caveats regarding this approach. Using our approach means a -non-trivial amount of functionality is reloaded on page change. A similar -amount of reloading occurs when using `window.InstantClick.on('change', someFunction)`. +non-trivial amount of functionality is reloaded on page change. A similar amount +of reloading occurs when using `window.InstantClick.on('change', someFunction)`. This results in code that looks something like this: ```javascript @@ -92,9 +106,9 @@ that front is welcome! ## Articles (or posts) Articles are the primary form of user generated content in the application. An -Article has many comments and taggings through the acts-as-taggable gem, -belongs to a single user (and possibly an organization), and is the core unit -of content. +Article has many comments and taggings through the acts-as-taggable gem, belongs +to a single user (and possibly an organization), and is the core unit of +content. ## Comments @@ -149,5 +163,6 @@ This is far from a complete view of the app, but it covers a few core concepts. [fastly]: https://www.fastly.com/ [rails_caching]: https://guides.rubyonrails.org/caching_with_rails.html [preact]: https://preactjs.com/ -[dynamic_imports]: https://dev.to/goenning/how-we-reduced-our-initial-jscss-size-by-67-3ac0 +[dynamic_imports]: + https://dev.to/goenning/how-we-reduced-our-initial-jscss-size-by-67-3ac0 [fastly_rails]: https://github.com/fastly/fastly-rails diff --git a/public/404.html b/public/404.html index 9ce5dfcc6..43ba4beb8 100644 --- a/public/404.html +++ b/public/404.html @@ -4,6 +4,10 @@ The page you were looking for doesn't exist (404) @@ -58,13 +63,15 @@ -
-
- 500 error -

500 Error

-

It's not you, it's me.

+
+ -

Sorry you're hitting this page.
Please help us by submitting a bug report.
Thank you!

diff --git a/public/offline.html b/public/offline.html index 070d04d97..25a253cd7 100644 --- a/public/offline.html +++ b/public/offline.html @@ -92,7 +92,7 @@ .rainbow-logo { border-radius: 10%; - max-width: 50%; + max-width: 40%; max-height: 400px; } diff --git a/spec/requests/async_info_spec.rb b/spec/requests/async_info_spec.rb index a228d08b2..1cf7c27fa 100644 --- a/spec/requests/async_info_spec.rb +++ b/spec/requests/async_info_spec.rb @@ -30,6 +30,13 @@ RSpec.describe "AsyncInfo", type: :request do end end + describe "GET /async_info/shell_version" do + it "returns shell_version" do + get "/async_info/shell_version" + expect(response.body).to include("version") + end + end + describe "#remember_user_token" do # We require the remember_user_token key bc we also use it for caching in Fastly # If this key changes, Fastly needs to be updated diff --git a/spec/requests/service_worker_spec.rb b/spec/requests/service_worker_spec.rb index c319bac0f..26b127c87 100644 --- a/spec/requests/service_worker_spec.rb +++ b/spec/requests/service_worker_spec.rb @@ -4,7 +4,7 @@ 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("var CACHE_VERSION") + expect(response.body).to include("Serviceworkers file") end it "renders javascript file" do diff --git a/spec/system/comments/user_delete_a_comment_spec.rb b/spec/system/comments/user_delete_a_comment_spec.rb index d8ff31b89..863bb618b 100644 --- a/spec/system/comments/user_delete_a_comment_spec.rb +++ b/spec/system/comments/user_delete_a_comment_spec.rb @@ -15,7 +15,7 @@ RSpec.describe "Deleting Comment", type: :system, js: true do it "works" do visit "/" visit comment.path + "/delete_confirm" - click_link("DELETE") + click_button("DELETE") expect(page).to have_current_path(article.path) end end