0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/lib/application_layout_preloader.rb
Martin Brennan 2fdc9af28e
FEATURE: Add upcoming change CSS classes to <body> (#36586)
This commit introduces automatic CSS classes added to
the <body> based on the upcoming changes the current visitor
(logged in or anon user) is affected by. This allows
designers and theme developers to easily style the UI
based on upcoming changes.

For example a change called `enable_chat_channel_starring`
would be `uc-enable-chat-channel-starring` on the body.

This commit also refactors the way we send upcoming change
data to the UI for the current user, because it also needs
to work for anon users. Only upcoming changes enabled for
Everyone will be added to the <body> for anon users. Logged
in in users will get classes for all upcoming changes
they are affected by based on their groups or changes enabled
for Everyone.
2025-12-11 11:09:09 +10:00

160 lines
4.6 KiB
Ruby
Vendored

# frozen_string_literal: true
class ApplicationLayoutPreloader
include ReadOnlyMixin
def self.banner_json_cache
@banner_json_cache ||= DistributedCache.new("banner_json")
end
def initialize(guardian:, theme_id:, theme_target:, login_method:)
@guardian = guardian
@theme_id = theme_id
@theme_target = theme_target
@login_method = login_method
@preloaded = {}
end
def store_preloaded(key, json)
# I dislike that there is a gsub as opposed to a gsub!
# but we can not be mucking with user input, I wonder if there is a way
# to inject this safety deeper in the library or even in AM serializer
@preloaded[key] = json.gsub("</", "<\\/")
end
def preloaded_data
preload_anonymous_data
preload_upcoming_change_data(@guardian.user)
if @guardian.authenticated?
@guardian.user.sync_notification_channel_position
preload_current_user_data
end
@preloaded
end
def banner_json
return "{}" if !@guardian.authenticated? && SiteSetting.login_required?
self
.class
.banner_json_cache
.defer_get_set("json") do
topic = Topic.where(archetype: Archetype.banner).first
banner = topic.present? ? topic.banner(@guardian) : {}
MultiJson.dump(banner)
end
end
def custom_html_json
data =
if @theme_id.present?
{
top: Theme.lookup_field(@theme_id, @theme_target, "after_header"),
footer: Theme.lookup_field(@theme_id, @theme_target, "footer"),
}
else
{}
end
data.merge! DiscoursePluginRegistry.custom_html if DiscoursePluginRegistry.custom_html
DiscoursePluginRegistry.html_builders.each do |name, _|
if name.start_with?("client:")
data[name.sub(/\Aclient:/, "")] = DiscoursePluginRegistry.build_html(name, self)
end
end
MultiJson.dump(data)
end
private
def preload_current_user_data
@preloaded["currentUser"] = MultiJson.dump(
CurrentUserSerializer.new(
@guardian.user,
scope: @guardian,
root: false,
login_method: @login_method,
),
)
report = TopicTrackingState.report(@guardian.user)
serializer = TopicTrackingStateSerializer.new(report, scope: @guardian, root: false)
hash = serializer.as_json
@preloaded["topicTrackingStates"] = MultiJson.dump(hash[:data])
@preloaded["topicTrackingStateMeta"] = MultiJson.dump(hash[:meta])
if @guardian.is_admin?
# This is used in the wizard so we can preload fonts using the FontMap JS API.
@preloaded["fontMap"] = MultiJson.dump(load_font_map)
# Used to show plugin-specific admin routes in the sidebar.
@preloaded["visiblePlugins"] = MultiJson.dump(
Discourse
.plugins_sorted_by_name(enabled_only: false)
.map do |plugin|
{
name: plugin.name.downcase,
humanized_name: plugin.humanized_name,
admin_route: plugin.full_admin_route,
enabled: plugin.enabled?,
description: plugin.metadata.about,
}
end,
)
end
end
def preload_anonymous_data
check_readonly_mode if @readonly_mode.nil?
@preloaded["site"] = Site.json_for(@guardian)
@preloaded["siteSettings"] = SiteSetting.client_settings_json
@preloaded["themeSiteSettingOverrides"] = SiteSetting.theme_site_settings_json(@theme_id)
@preloaded["customHTML"] = custom_html_json
@preloaded["banner"] = banner_json
@preloaded["customEmoji"] = custom_emoji
@preloaded["isReadOnly"] = @readonly_mode.to_json
@preloaded["isStaffWritesOnly"] = @staff_writes_only_mode.to_json
@preloaded["activatedThemes"] = activated_themes_json
end
def preload_upcoming_change_data(user)
@preloaded["upcomingChanges"] = SiteSetting
.upcoming_change_site_settings
.each_with_object({}) do |upcoming_change, hash|
hash[upcoming_change] = UpcomingChanges.enabled_for_user?(upcoming_change, user)
end
.to_json
end
def activated_themes_json
id = @theme_id
return "{}" if id.blank?
ids = Theme.transform_ids(id)
Theme.where(id: ids).pluck(:id, :name).to_h.to_json
end
def load_font_map
DiscourseFonts
.fonts
.each_with_object({}) do |font, font_map|
next if !font[:variants]
font_map[font[:key]] = font[:variants].map do |v|
{
url: "#{Discourse.base_url}/fonts/#{v[:filename]}?v=#{DiscourseFonts::VERSION}",
weight: v[:weight],
}
end
end
end
def custom_emoji
serializer = ActiveModel::ArraySerializer.new(Emoji.custom, each_serializer: EmojiSerializer)
MultiJson.dump(serializer)
end
end