docbrown/app/helpers/application_helper.rb
Suzanne Aitchison f1df37f431
Add "only display when logged out" as an option on Navigation Links (#18275)
* add new column with enum value

* refactor to use display_to column

* add some more specs to application_helper

* test for the backfill DUS

* fix line length

* refactor
2022-08-08 10:55:24 +01:00

398 lines
14 KiB
Ruby

module ApplicationHelper
LARGE_USERBASE_THRESHOLD = 1000
# @return [Hash<String, String>] the key is the timeframe name and the corresponding is the
# translated label.
def subtitles
{
"week" => I18n.t("helpers.application_helper.subtitle.week"),
"month" => I18n.t("helpers.application_helper.subtitle.month"),
"year" => I18n.t("helpers.application_helper.subtitle.year"),
"infinity" => I18n.t("helpers.application_helper.subtitle.infinity"),
"latest" => I18n.t("helpers.application_helper.subtitle.latest")
}
end
# Provides a status string for HTML data attributes.
#
# @return [String] "logged-in" or "logged-out" depending on the requesting user's current signin
# status.
def user_logged_in_status
user_signed_in? ? "logged-in" : "logged-out"
end
# Provides a string for HTML data attributes; comprised of the response's Rails controller name
# and action name.
#
# @return [String]
def current_page
"#{controller_name}-#{controller.action_name}"
end
# Answers the simple question "Should we show this link?"
#
# @param link [NavigationLink]
#
# @return [TrueClass] true when we should render the given link.
# @return [FalseClass] false when we should **not** render the given link.
def display_navigation_link?(link:)
# This is a quick short-circuit; we already have the link. So don't bother asking the "Is this
# feature enabled" question if the given link requires a logged in/out state
# that doesn't match the user's current state.
return false if link.display_to_logged_in? && !user_signed_in?
return false if link.display_to_logged_out? && user_signed_in?
return true if navigation_link_is_for_an_enabled_feature?(link: link)
false
end
# @param link [NavigationLink]
#
# @note [@jeremyf] - making an assumption, namely that the only navigation oriented feature is the
# Listing. If this changes, adjust this method accordingly. Normally I like to have method return
def navigation_link_is_for_an_enabled_feature?(link:)
return true if Listing.feature_enabled?
# The "/listings" is an assumption on the routing. So let's first try :listings_path.
listings_url = URL.url(try(:listings_path) || "/listings")
return false if listings_url == URL.url(link.url)
true
end
# @return [String] a string conformant to HTML class attribute
# @see ApplicationHelper#current_page defines the fallback
#
# @note This tests for the existence of two specific instance variables.
#
# rubocop:disable Rails/HelperInstanceVariable
def view_class
if @podcast_episode_show # custom due to edge cases
"stories stories-show podcast_episodes-show"
elsif @story_show
"stories stories-show"
else
"#{controller_name} #{current_page}"
end
end
# rubocop:enable Rails/HelperInstanceVariable
# This function derives the appropriate "title" given the page_title. Further it assigns the
# `:title` content region.
#
# @param page_title [String] the proposed title
# @return [String] the derived title based on method implementation.
#
# @note In addition to returning the derived title, this function will call
# {ActionView::Helpers::CaptureHelper#content_for} to set the `:title` "content" region.
#
# @see https://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html#method-i-content_for
# API docs for `#content_for`
# @see https://guides.rubyonrails.org/layouts_and_rendering.html#using-the-content-for-method
# Guide docs for `#content_for`
def title(page_title)
derived_title = if page_title.include?(community_name)
page_title
elsif user_signed_in?
"#{page_title} - #{community_name} #{community_emoji}"
else
"#{page_title} - #{community_name}"
end
content_for(:title) { derived_title }
derived_title
end
# Derives a timeframe specific title based on the given parameters.
#
# @param page_title [String] the proposed title
# @param timeframe [String] a timeframe
# @param content_for [Boolean] when true, pass the derived title to the `#title` method.
#
# @see ApplicationHelper#subtitles `#subtitles` method for details around the timeframe.
# @see ApplicationHelper#title `#title` method for details on how we convert this title.
def title_with_timeframe(page_title:, timeframe:, content_for: false)
if timeframe.blank? || subtitles[timeframe].blank?
return content_for ? title(page_title) : page_title
end
title_text = I18n.t("helpers.application_helper.title_text", title: page_title,
timeframe: subtitles.fetch(timeframe))
content_for ? title(title_text) : title_text
end
# @param url [String, #presence]
# @param width [Integer]
# @param quality [Integer]
# @param fetch_format [String]
# @param random_fallback [Boolean]
#
# @return [String] if we have a URL or a fallback image
# @return [NilClass] if there is no given URL nor a fallback image
# @note This method uses different logic than {ApplicationHelper#optimized_image_tag}
def optimized_image_url(url, width: 500, quality: 80, fetch_format: "auto", random_fallback: true)
fallback_image = asset_path("#{rand(1..40)}.png") if random_fallback
return unless (image_url = url.presence || fallback_image)
normalized_url = Addressable::URI.parse(image_url).normalize.to_s
Images::Optimizer.call(normalized_url, width: width, quality: quality, fetch_format: fetch_format)
end
# @todo Should this use {ApplicationHelper#optimized_image_url} logic?
def optimized_image_tag(image_url, optimizer_options: {}, image_options: {})
image_options[:width] ||= optimizer_options[:width]
image_options[:height] ||= optimizer_options[:height]
updated_image_url = Images::Optimizer.call(image_url, **optimizer_options)
image_tag(updated_image_url, image_options)
end
def cloud_cover_url(url)
CloudCoverUrl.new(url).call
end
def tag_colors(tag)
Rails.cache.fetch("view-helper-#{tag}/tag_colors", expires_in: 5.hours) do
if (found_tag = Tag.select(%i[bg_color_hex text_color_hex]).find_by(name: tag))
{ background: found_tag.bg_color_hex, color: found_tag.text_color_hex }
else
{ background: "#d6d9e0", color: "#606570" }
end
end
end
def any_enabled_auth_providers?
authentication_enabled_providers.any?
end
def beautified_url(url)
url.sub(%r{\A((https?|ftp):/)?/}, "").sub(/\?.*/, "").chomp("/")
rescue StandardError
url
end
def org_bg_or_white(org)
org&.bg_color_hex ? org.bg_color_hex : "#ffffff"
end
def sanitize_rendered_markdown(processed_html)
ActionController::Base.helpers.sanitize processed_html,
scrubber: RenderedMarkdownScrubber.new
end
def sanitized_sidebar(text)
ActionController::Base.helpers.sanitize simple_format(text),
tags: MarkdownProcessor::AllowedTags::SIDEBAR
end
def follow_button(followable, style = "full", classes = "")
return if followable == Users::DeletedUser
user_follow = followable.instance_of?(User) ? "follow-user" : ""
followable_type = followable.class_name
followable_name = followable.name
tag.button(
I18n.t("helpers.application_helper.follow.text.#{followable_type}",
default: I18n.t("helpers.application_helper.follow.text.default")),
name: :button,
type: :button,
data: {
info: DataInfo.to_json(object: followable, className: followable_type, style: style)
},
class: "crayons-btn follow-action-button whitespace-nowrap #{classes} #{user_follow}",
aria: {
label: I18n.t("helpers.application_helper.follow.aria_label.#{followable_type}",
name: followable_name,
default: I18n.t("helpers.application_helper.follow.aria_label.default", type: followable_type,
name: followable_name)),
pressed: "false"
},
)
end
def user_colors_style(user)
"border: 2px solid #{user.decorate.darker_color}; \
box-shadow: 5px 6px 0px #{user.decorate.darker_color}"
end
def user_colors(user)
user.decorate.enriched_colors
end
def timeframe_check(given_timeframe)
params[:timeframe] == given_timeframe
end
def list_path
return "" if params[:tag].blank?
"/t/#{params[:tag]}"
end
def community_name
@community_name ||= Settings::Community.community_name
end
def community_emoji
@community_emoji ||= Settings::Community.community_emoji
end
def release_adjusted_cache_key(path)
release_footprint = ForemInstance.deployed_at
return path if release_footprint.blank?
"#{path}-#{params[:locale]}-#{release_footprint}-#{Settings::General.admin_action_taken_at.rfc3339}"
end
def copyright_notice
start_year = Settings::Community.copyright_start_year.to_s
current_year = Time.current.year.to_s
return start_year if current_year == start_year
return current_year if start_year.strip.length < 4 # 978 is not a valid year!
"#{start_year} - #{current_year}"
end
def collection_link(collection, **kwargs)
size_string = I18n.t("views.articles.series.size", count: collection.articles.published.size)
body = if collection.slug.present?
I18n.t("views.articles.series.subtitle", slug: collection.slug,
size: size_string)
else
size_string
end
link_to body, collection.path, **kwargs
end
def contact_link(text: nil, additional_info: nil)
email = ForemInstance.contact_email
mail_to email, text || email, additional_info
end
def community_members_label
Settings::Community.member_label.pluralize
end
def meta_keywords_default
return if Settings::General.meta_keywords[:default].blank?
tag.meta name: "keywords", content: Settings::General.meta_keywords[:default]
end
def meta_keywords_article(article_tags = nil)
return if Settings::General.meta_keywords[:article].blank?
content = if article_tags.present?
"#{article_tags}, #{Settings::General.meta_keywords[:article]}"
else
Settings::General.meta_keywords[:article]
end
tag.meta name: "keywords", content: content
end
def meta_keywords_tag(tag_name)
return if Settings::General.meta_keywords[:tag].blank?
tag.meta name: "keywords", content: "#{Settings::General.meta_keywords[:tag]}, #{tag_name}"
end
def app_url(uri = nil)
URL.url(uri)
end
def article_url(article)
URL.article(article)
end
def comment_url(comment)
URL.comment(comment)
end
def reaction_url(reaction)
URL.reaction(reaction)
end
def tag_url(tag, page = 1)
URL.tag(tag, page)
end
def user_url(user)
URL.user(user)
end
def organization_url(organization)
URL.organization(organization)
end
def estimated_user_count
User.registered.estimated_count
end
def display_estimated_user_count?
estimated_user_count > LARGE_USERBASE_THRESHOLD
end
def admin_config_label(method, content = nil, model: Settings::General)
content ||= tag.span(method.to_s.humanize)
label_prefix = model.name.split("::").map(&:underscore).join("_")
tag.label(content, class: "site-config__label crayons-field__label", for: "#{label_prefix}_#{method}")
end
def admin_config_description(content, **opts)
tag.p(content, class: "crayons-field__description", **opts) unless content.empty?
end
def role_display_name(role)
role.name.titlecase
end
def render_tag_link(tag, filled: false, monochrome: false, classes: "")
color = tag_colors(tag)[:background].presence || Settings::UserExperience.primary_brand_color_hex
color_faded = Color::CompareHex.new([color]).opacity(0.1)
label = safe_join([content_tag(:span, "#", class: "crayons-tag__prefix"), tag])
options = {
class: "crayons-tag #{'crayons-tag--filled' if filled} #{'crayons-tag--monochrome' if monochrome} #{classes}",
style: "
--tag-bg: #{color_faded};
--tag-prefix: #{color};
--tag-bg-hover: #{color_faded};
--tag-prefix-hover: #{color};
"
}
link_to(label, tag_path(tag), options)
end
def creator_settings_form?
return unless User.with_role(:creator).any?
creator = User.with_role(:creator).first
!creator.checked_code_of_conduct && !creator.checked_terms_and_conditions
end
# This function is responsible for adding a policy class (and possibly the hidden class). It's
# applying some shortcuts to reduce the likelihood of any Cumulative Layout Shift.
#
# @param name [String,Symbol] the HTML element name (e.g. "li", "div", "a")
# @param record [Object] the record for which we're testing a policy
# @param query [Symbol, String] the query we're running on the policy
# @param kwargs [Hash] The arguments pass, with modifications to the given :class (see
# implementation details).
#
# @yield the body of the HTML element
#
# @see ApplicationPolicy.dom_class_for
# @see https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag
# @see ./app/javascript/packs/applyApplicationPolicyToggles.js
def application_policy_content_tag(name, record:, query:, **kwargs, &block)
dom_class = kwargs.delete(:class) || kwargs.delete("class") || ""
dom_class += " #{ApplicationPolicy.dom_classes_for(record: record, query: query)}"
content_tag(name, class: dom_class, **kwargs, &block)
end
end