From 52013eac1f5505c254349a58e4f272e2588af6a3 Mon Sep 17 00:00:00 2001 From: evazion Date: Thu, 9 Dec 2021 19:52:02 -0600 Subject: [PATCH] posts: use low quality thumbnails when Save-Data header is set. When the Save-Data HTTP header is present, disable high quality (2x pixel density) thumbnails. This is normally set when "Data Saver mode" is enabled on Android, or "Lite mode" is enabled in Chrome. This setting can also be set using the `save_data` URL param or HTTP cookie. This is mainly for testing. The tag has a `current-user-save-data` data attribute that indicates whether save data mode is on. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Save-Data https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/save-data/#the_save-data_request_header https://source.android.com/devices/tech/connect/data-saver --- app/components/post_preview_component.rb | 7 +++++-- .../post_preview_component.html.erb | 16 +++++++++------- app/helpers/application_helper.rb | 5 +++-- app/logical/current_user.rb | 5 +++-- app/logical/session_loader.rb | 8 ++++++++ 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/app/components/post_preview_component.rb b/app/components/post_preview_component.rb index ee2294406..bbd1df72c 100644 --- a/app/components/post_preview_component.rb +++ b/app/components/post_preview_component.rb @@ -7,7 +7,7 @@ class PostPreviewComponent < ApplicationComponent with_collection_parameter :post - attr_reader :post, :tags, :size, :show_deleted, :link_target, :pool, :similarity, :recommended, :show_votes, :fit, :show_size, :current_user, :options + attr_reader :post, :tags, :size, :show_deleted, :link_target, :pool, :similarity, :recommended, :show_votes, :fit, :show_size, :save_data, :current_user, :options delegate :external_link_to, :time_ago_in_words_tagged, :duration_to_hhmmss, :render_post_votes, :empty_heart_icon, :sound_icon, to: :helpers delegate :image_width, :image_height, :file_ext, :file_size, :duration, :is_animated?, to: :media_asset @@ -21,12 +21,14 @@ class PostPreviewComponent < ApplicationComponent # If false, hide thumbnails of deleted posts. # @param show_votes [Boolean] If true, show scores and vote buttons beneath the thumbnail. # @param show_size [Boolean] If true, show filesize and resolution beneath the thumbnail. + # @param save_data [Boolean] If true, save data by not serving higher quality thumbnails + # on 2x pixel density displays. Default: false. # @param link_target [ApplicationRecord] What the thumbnail links to (default: the post). # @param current_user [User] The current user. # @param fit [Symbol] If `:fixed`, make the thumbnail container a fixed size # (e.g. 180x180), even if the thumbnail image is smaller than that. If `:compact`, # make the thumbnail container shrink to the same size as the thumbnail image. - def initialize(post:, tags: "", size: DEFAULT_SIZE, show_deleted: false, show_votes: false, link_target: post, pool: nil, similarity: nil, recommended: nil, show_size: nil, fit: :compact, current_user: CurrentUser.user, **options) + def initialize(post:, tags: "", size: DEFAULT_SIZE, show_deleted: false, show_votes: false, link_target: post, pool: nil, similarity: nil, recommended: nil, show_size: nil, save_data: CurrentUser.save_data, fit: :compact, current_user: CurrentUser.user, **options) super @post = post @tags = tags.presence @@ -39,6 +41,7 @@ class PostPreviewComponent < ApplicationComponent @recommended = recommended @fit = fit @show_size = show_size + @save_data = save_data @current_user = current_user @options = options end diff --git a/app/components/post_preview_component/post_preview_component.html.erb b/app/components/post_preview_component/post_preview_component.html.erb index 558526a73..69c7a3a04 100644 --- a/app/components/post_preview_component/post_preview_component.html.erb +++ b/app/components/post_preview_component/post_preview_component.html.erb @@ -14,13 +14,15 @@ <% end %> - <% case size %> - <% when "150" %> - <%# no-op %> - <% when "180" %> - <%= tag.source type: "image/jpeg", srcset: "#{media_asset.variant("180x180").file_url} 1x, #{media_asset.variant("360x360").file_url} 2x" %> - <% else # 225 to 360 %> - <%= tag.source type: "image/webp", srcset: "#{media_asset.variant("360x360").file_url} 1x, #{media_asset.variant("720x720").file_url} 2x" %> + <% unless save_data %> + <% case size %> + <% when "150" %> + <%# no-op %> + <% when "180" %> + <%= tag.source type: "image/jpeg", srcset: "#{media_asset.variant("180x180").file_url} 1x, #{media_asset.variant("360x360").file_url} 2x" %> + <% else # 225 to 360 %> + <%= tag.source type: "image/webp", srcset: "#{media_asset.variant("360x360").file_url} 1x, #{media_asset.variant("720x720").file_url} 2x" %> + <% end %> <% end %> <%= tag.img src: variant.file_url, width: variant.width, height: variant.height, class: "post-preview-image", title: tooltip, alt: "post ##{post.id}", crossorigin: "anonymous" -%> diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 74d5da472..485f66863 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -252,7 +252,7 @@ module ApplicationHelper render "table_builder/table", table: table end - def body_attributes(user, params, current_item = nil) + def body_attributes(current_user, params, current_item = nil) controller_param = params[:controller].parameterize.dasherize action_param = params[:action].parameterize.dasherize @@ -265,7 +265,8 @@ module ApplicationHelper action: action_param, layout: controller.class.send(:_layout), "current-user-ip-addr": request.remote_ip, - **current_user_data_attributes(user), + "current-user-save-data": CurrentUser.save_data, + **current_user_data_attributes(current_user), **cookie_data_attributes, **current_item_data_attributes(current_item), } diff --git a/app/logical/current_user.rb b/app/logical/current_user.rb index 6846b3f52..0ca78ce4f 100644 --- a/app/logical/current_user.rb +++ b/app/logical/current_user.rb @@ -1,5 +1,6 @@ # A global variable containing the current user, the current IP address, the -# current user's country code, and whether safe mode is enabled. +# current user's country code, whether safe mode is enabled, and whether +# save-data mode is enabled. # # The current user is set during a request by {ApplicationController#set_current_user}, # which calls {SessionLoader#load}. The current user will not be set outside of @@ -11,7 +12,7 @@ # @see ApplicationController#set_current_user # @see SessionLoader#load class CurrentUser < ActiveSupport::CurrentAttributes - attribute :user, :ip_addr, :country, :safe_mode + attribute :user, :ip_addr, :country, :safe_mode, :save_data alias_method :safe_mode?, :safe_mode delegate :id, to: :user, allow_nil: true diff --git a/app/logical/session_loader.rb b/app/logical/session_loader.rb index 2fe8c62fa..636d6b550 100644 --- a/app/logical/session_loader.rb +++ b/app/logical/session_loader.rb @@ -80,6 +80,7 @@ class SessionLoader set_time_zone set_country set_safe_mode + set_save_data_mode initialize_session_cookies CurrentUser.user.unban! if CurrentUser.user.ban_expired? ensure @@ -177,6 +178,13 @@ class SessionLoader CurrentUser.safe_mode = safe_mode end + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Save-Data + # https://www.keycdn.com/blog/save-data + def set_save_data_mode + save_data = params[:save_data].presence || request.cookies[:save_data].presence || request.headers["Save-Data"].presence || "false" + CurrentUser.save_data = save_data.truthy? + end + def initialize_session_cookies session.options[:expire_after] = 20.years session[:started_at] ||= Time.now.utc.to_s