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 <body> 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
This commit is contained in:
evazion
2021-12-09 19:52:02 -06:00
parent 7dbde7bc14
commit 52013eac1f
5 changed files with 28 additions and 13 deletions

View File

@@ -7,7 +7,7 @@ class PostPreviewComponent < ApplicationComponent
with_collection_parameter :post 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 :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 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. # If false, hide thumbnails of deleted posts.
# @param show_votes [Boolean] If true, show scores and vote buttons beneath the thumbnail. # @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 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 link_target [ApplicationRecord] What the thumbnail links to (default: the post).
# @param current_user [User] The current user. # @param current_user [User] The current user.
# @param fit [Symbol] If `:fixed`, make the thumbnail container a fixed size # @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`, # (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. # 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 super
@post = post @post = post
@tags = tags.presence @tags = tags.presence
@@ -39,6 +41,7 @@ class PostPreviewComponent < ApplicationComponent
@recommended = recommended @recommended = recommended
@fit = fit @fit = fit
@show_size = show_size @show_size = show_size
@save_data = save_data
@current_user = current_user @current_user = current_user
@options = options @options = options
end end

View File

@@ -14,13 +14,15 @@
<% end %> <% end %>
<picture> <picture>
<% case size %> <% unless save_data %>
<% when "150" %> <% case size %>
<%# no-op %> <% when "150" %>
<% when "180" %> <%# no-op %>
<%= tag.source type: "image/jpeg", srcset: "#{media_asset.variant("180x180").file_url} 1x, #{media_asset.variant("360x360").file_url} 2x" %> <% when "180" %>
<% else # 225 to 360 %> <%= tag.source type: "image/jpeg", srcset: "#{media_asset.variant("180x180").file_url} 1x, #{media_asset.variant("360x360").file_url} 2x" %>
<%= tag.source type: "image/webp", srcset: "#{media_asset.variant("360x360").file_url} 1x, #{media_asset.variant("720x720").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 %> <% 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" -%> <%= tag.img src: variant.file_url, width: variant.width, height: variant.height, class: "post-preview-image", title: tooltip, alt: "post ##{post.id}", crossorigin: "anonymous" -%>

View File

@@ -252,7 +252,7 @@ module ApplicationHelper
render "table_builder/table", table: table render "table_builder/table", table: table
end end
def body_attributes(user, params, current_item = nil) def body_attributes(current_user, params, current_item = nil)
controller_param = params[:controller].parameterize.dasherize controller_param = params[:controller].parameterize.dasherize
action_param = params[:action].parameterize.dasherize action_param = params[:action].parameterize.dasherize
@@ -265,7 +265,8 @@ module ApplicationHelper
action: action_param, action: action_param,
layout: controller.class.send(:_layout), layout: controller.class.send(:_layout),
"current-user-ip-addr": request.remote_ip, "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, **cookie_data_attributes,
**current_item_data_attributes(current_item), **current_item_data_attributes(current_item),
} }

View File

@@ -1,5 +1,6 @@
# A global variable containing the current user, the current IP address, the # 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}, # 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 # which calls {SessionLoader#load}. The current user will not be set outside of
@@ -11,7 +12,7 @@
# @see ApplicationController#set_current_user # @see ApplicationController#set_current_user
# @see SessionLoader#load # @see SessionLoader#load
class CurrentUser < ActiveSupport::CurrentAttributes 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 alias_method :safe_mode?, :safe_mode
delegate :id, to: :user, allow_nil: true delegate :id, to: :user, allow_nil: true

View File

@@ -80,6 +80,7 @@ class SessionLoader
set_time_zone set_time_zone
set_country set_country
set_safe_mode set_safe_mode
set_save_data_mode
initialize_session_cookies initialize_session_cookies
CurrentUser.user.unban! if CurrentUser.user.ban_expired? CurrentUser.user.unban! if CurrentUser.user.ban_expired?
ensure ensure
@@ -177,6 +178,13 @@ class SessionLoader
CurrentUser.safe_mode = safe_mode CurrentUser.safe_mode = safe_mode
end 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 def initialize_session_cookies
session.options[:expire_after] = 20.years session.options[:expire_after] = 20.years
session[:started_at] ||= Time.now.utc.to_s session[:started_at] ||= Time.now.utc.to_s