diff --git a/app/assets/javascripts/discourse/app/instance-initializers/category-background-css-generator.js b/app/assets/javascripts/discourse/app/instance-initializers/category-background-css-generator.js new file mode 100644 index 00000000000..5243bbbd4da --- /dev/null +++ b/app/assets/javascripts/discourse/app/instance-initializers/category-background-css-generator.js @@ -0,0 +1,55 @@ +import { getURLWithCDN } from "discourse-common/lib/get-url"; + +export default { + after: "register-hashtag-types", + + initialize(owner) { + this.session = owner.lookup("service:session"); + this.site = owner.lookup("service:site"); + + if (!this.site.categories?.length) { + return; + } + + const css = []; + const darkCss = []; + + this.site.categories.forEach((category) => { + const lightUrl = category.uploaded_background?.url; + const darkUrl = + this.session.defaultColorSchemeIsDark || this.session.darkModeAvailable + ? category.uploaded_background_dark?.url + : null; + const defaultUrl = + darkUrl && this.session.defaultColorSchemeIsDark ? darkUrl : lightUrl; + + if (defaultUrl) { + const url = getURLWithCDN(defaultUrl); + css.push( + `body.category-${category.fullSlug} { background-image: url(${url}); }` + ); + } + + if (darkUrl && defaultUrl !== darkUrl) { + const url = getURLWithCDN(darkUrl); + darkCss.push( + `body.category-${category.fullSlug} { background-image: url(${url}); }` + ); + } + }); + + if (darkCss.length > 0) { + css.push("@media (prefers-color-scheme: dark) {", ...darkCss, "}"); + } + + const cssTag = document.createElement("style"); + cssTag.type = "text/css"; + cssTag.id = "category-background-css-generator"; + cssTag.innerHTML = css.join("\n"); + document.head.appendChild(cssTag); + }, + + teardown() { + document.querySelector("#category-background-css-generator")?.remove(); + }, +}; diff --git a/app/assets/javascripts/discourse/tests/acceptance/category-background-css-generator-test.js b/app/assets/javascripts/discourse/tests/acceptance/category-background-css-generator-test.js new file mode 100644 index 00000000000..7ba43e8578c --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/category-background-css-generator-test.js @@ -0,0 +1,132 @@ +import { visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import Session from "discourse/models/session"; +import { acceptance } from "discourse/tests/helpers/qunit-helpers"; + +const SITE_DATA = { + categories: [ + { + id: 1, + color: "ff0000", + text_color: "ffffff", + name: "category1", + slug: "foo", + uploaded_background: { + id: 54, + url: "/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png", + width: 1024, + height: 768, + }, + }, + { + id: 2, + color: "333", + text_color: "ffffff", + name: "category2", + slug: "bar", + uploaded_background_dark: { + id: 25, + url: "/uploads/default/original/1X/f9fdb0ad108f2aed178c40f351bbb2c7cb2571e3.png", + width: 1024, + height: 768, + }, + }, + { + id: 4, + color: "2B81AF", + text_color: "ffffff", + parent_category_id: 1, + name: "category3", + slug: "baz", + uploaded_background: { + id: 11, + url: "/uploads/default/original/1X/684c104edc18a7e9cef1fa31f41215f3eec5d92b.png", + width: 1024, + height: 768, + }, + uploaded_background_dark: { + id: 19, + url: "/uploads/default/original/1X/89b1a2641e91604c32b21db496be11dba7a253e6.png", + width: 1024, + height: 768, + }, + }, + ], +}; + +acceptance("Category Background CSS Generator", function (needs) { + needs.user(); + needs.site(SITE_DATA); + + test("CSS classes are generated", async function (assert) { + await visit("/"); + + assert.equal( + document.querySelector("#category-background-css-generator").innerHTML, + "body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" + + "body.category-foo-baz { background-image: url(/uploads/default/original/1X/684c104edc18a7e9cef1fa31f41215f3eec5d92b.png); }" + ); + }); +}); + +acceptance("Category Background CSS Generator (dark)", function (needs) { + needs.user(); + needs.site(SITE_DATA); + + needs.hooks.beforeEach(function () { + const session = Session.current(); + session.set("darkModeAvailable", true); + session.set("defaultColorSchemeIsDark", false); + }); + + needs.hooks.afterEach(function () { + const session = Session.current(); + session.set("darkModeAvailable", null); + session.set("defaultColorSchemeIsDark", null); + }); + + test("CSS classes are generated", async function (assert) { + await visit("/"); + + assert.equal( + document.querySelector("#category-background-css-generator").innerHTML, + "body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" + + "body.category-foo-baz { background-image: url(/uploads/default/original/1X/684c104edc18a7e9cef1fa31f41215f3eec5d92b.png); }\n" + + "@media (prefers-color-scheme: dark) {\n" + + "body.category-bar { background-image: url(/uploads/default/original/1X/f9fdb0ad108f2aed178c40f351bbb2c7cb2571e3.png); }\n" + + "body.category-foo-baz { background-image: url(/uploads/default/original/1X/89b1a2641e91604c32b21db496be11dba7a253e6.png); }\n" + + "}" + ); + }); +}); + +acceptance( + "Category Background CSS Generator (dark is default)", + function (needs) { + needs.user(); + needs.site(SITE_DATA); + + needs.hooks.beforeEach(function () { + const session = Session.current(); + session.set("darkModeAvailable", true); + session.set("defaultColorSchemeIsDark", true); + }); + + needs.hooks.afterEach(function () { + const session = Session.current(); + session.set("darkModeAvailable", null); + session.set("defaultColorSchemeIsDark", null); + }); + + test("CSS classes are generated", async function (assert) { + await visit("/"); + + assert.equal( + document.querySelector("#category-background-css-generator").innerHTML, + "body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" + + "body.category-bar { background-image: url(/uploads/default/original/1X/f9fdb0ad108f2aed178c40f351bbb2c7cb2571e3.png); }\n" + + "body.category-foo-baz { background-image: url(/uploads/default/original/1X/89b1a2641e91604c32b21db496be11dba7a253e6.png); }" + ); + }); + } +); diff --git a/lib/stylesheet/compiler.rb b/lib/stylesheet/compiler.rb index 1f839ae6ef8..583f69dbbd0 100644 --- a/lib/stylesheet/compiler.rb +++ b/lib/stylesheet/compiler.rb @@ -34,7 +34,6 @@ module Stylesheet when Stylesheet::Manager::COLOR_SCHEME_STYLESHEET file += importer.import_color_definitions file += importer.import_wcag_overrides - file += importer.category_backgrounds(options[:color_scheme_id]) file += importer.font end end diff --git a/lib/stylesheet/importer.rb b/lib/stylesheet/importer.rb index 90fdcdddf1e..daab2cd31a4 100644 --- a/lib/stylesheet/importer.rb +++ b/lib/stylesheet/importer.rb @@ -95,20 +95,6 @@ module Stylesheet contents end - def category_backgrounds(color_scheme_id) - is_dark_color_scheme = - color_scheme_id.present? && ColorScheme.find_by_id(color_scheme_id)&.is_dark? - - contents = +"" - Category - .where("uploaded_background_id IS NOT NULL") - .each do |c| - contents << category_css(c, is_dark_color_scheme) if c.uploaded_background&.url.present? - end - - contents - end - def import_color_definitions contents = +"" DiscoursePluginRegistry.color_definition_stylesheets.each do |name, path| @@ -220,22 +206,6 @@ module Stylesheet @theme == :nil ? nil : @theme end - def category_css(category, is_dark_color_scheme) - full_slug = category.full_slug.split("-")[0..-2].join("-") - - # in case we're using a dark color scheme, we define the background using the dark image - # if one is available. Otherwise, we use the light image by default. - if is_dark_color_scheme && category.uploaded_background_dark&.url.present? - return category_background_css(full_slug, category.uploaded_background_dark.url) - end - - category_background_css(full_slug, category.uploaded_background.url) - end - - def category_background_css(full_slug, background_url) - "body.category-#{full_slug} { background-image: url(#{upload_cdn_path(background_url)}) }" - end - def font_css(font) contents = +"" diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb index a437330dade..04b1de3b696 100644 --- a/lib/stylesheet/manager.rb +++ b/lib/stylesheet/manager.rb @@ -7,7 +7,7 @@ module Stylesheet end class Stylesheet::Manager - BASE_COMPILER_VERSION = 1 + BASE_COMPILER_VERSION = 2 CACHE_PATH = "tmp/stylesheet-cache" private_constant :CACHE_PATH diff --git a/lib/stylesheet/manager/builder.rb b/lib/stylesheet/manager/builder.rb index 8bebf711bad..738770de3ce 100644 --- a/lib/stylesheet/manager/builder.rb +++ b/lib/stylesheet/manager/builder.rb @@ -240,20 +240,13 @@ class Stylesheet::Manager::Builder def color_scheme_digest cs = @color_scheme || theme&.color_scheme - categories_updated = - Stylesheet::Manager - .cache - .defer_get_set("categories_updated") do - Category.where("uploaded_background_id IS NOT NULL").pluck(:updated_at).map(&:to_i).sum - end - fonts = "#{SiteSetting.base_font}-#{SiteSetting.heading_font}" digest_string = "#{current_hostname}-" - if cs || categories_updated > 0 + if cs theme_color_defs = resolve_baked_field(:common, :color_definitions) digest_string += - "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{categories_updated}-#{fonts}" + "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}" else digest_string += "defaults-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}" diff --git a/spec/lib/stylesheet/importer_spec.rb b/spec/lib/stylesheet/importer_spec.rb index 53f1fc3837c..0f495a10c4d 100644 --- a/spec/lib/stylesheet/importer_spec.rb +++ b/spec/lib/stylesheet/importer_spec.rb @@ -7,151 +7,6 @@ RSpec.describe Stylesheet::Importer do Stylesheet::Compiler.compile_asset(name, options)[0] end - describe "#category_backgrounds" do - it "uses the correct background image based in the color scheme" do - background = Fabricate(:upload) - background_dark = Fabricate(:upload) - - parent_category = Fabricate(:category) - category = - Fabricate( - :category, - parent_category_id: parent_category.id, - uploaded_background: background, - uploaded_background_dark: background_dark, - ) - - # light color schemes - ["Neutral", "Shades of Blue", "WCAG", "Summer", "Solarized Light"].each do |scheme_name| - scheme = ColorScheme.create_from_base(name: "Light Test", base_scheme_id: scheme_name) - - compiled_css = compile_css("color_definitions", { color_scheme_id: scheme.id }) - - expect(compiled_css).to include( - "body.category-#{parent_category.slug}-#{category.slug}{background-image:url(#{background.url})}", - ) - expect(compiled_css).not_to include(background_dark.url) - end - - # dark color schemes - [ - "Dark", - "Grey Amber", - "Latte", - "Dark Rose", - "WCAG Dark", - "Dracula", - "Solarized Dark", - ].each do |scheme_name| - scheme = ColorScheme.create_from_base(name: "Light Test", base_scheme_id: scheme_name) - - compiled_css = compile_css("color_definitions", { color_scheme_id: scheme.id }) - - expect(compiled_css).not_to include(background.url) - expect(compiled_css).to include( - "body.category-#{parent_category.slug}-#{category.slug}{background-image:url(#{background_dark.url})}", - ) - end - end - - it "applies CDN to background category images" do - expect(compile_css("color_definitions")).to_not include("body.category-") - - background = Fabricate(:upload) - background_dark = Fabricate(:upload) - - parent_category = Fabricate(:category) - category = - Fabricate( - :category, - parent_category_id: parent_category.id, - uploaded_background: background, - uploaded_background_dark: background_dark, - ) - - compiled_css = compile_css("color_definitions") - expect(compiled_css).to include( - "body.category-#{parent_category.slug}-#{category.slug}{background-image:url(#{background.url})}", - ) - - GlobalSetting.stubs(:cdn_url).returns("//awesome.cdn") - compiled_css = compile_css("color_definitions") - expect(compiled_css).to include( - "body.category-#{parent_category.slug}-#{category.slug}{background-image:url(//awesome.cdn#{background.url})}", - ) - end - - it "applies CDN to dark background category images" do - scheme = ColorScheme.create_from_base(name: "Dark Test", base_scheme_id: "Dark") - expect(compile_css("color_definitions", { color_scheme_id: scheme.id })).to_not include( - "body.category-", - ) - - background = Fabricate(:upload) - background_dark = Fabricate(:upload) - - parent_category = Fabricate(:category) - category = - Fabricate( - :category, - parent_category_id: parent_category.id, - uploaded_background: background, - uploaded_background_dark: background_dark, - ) - - compiled_css = compile_css("color_definitions", { color_scheme_id: scheme.id }) - expect(compiled_css).to include( - "body.category-#{parent_category.slug}-#{category.slug}{background-image:url(#{background_dark.url})}", - ) - - GlobalSetting.stubs(:cdn_url).returns("//awesome.cdn") - compiled_css = compile_css("color_definitions", { color_scheme_id: scheme.id }) - expect(compiled_css).to include( - "body.category-#{parent_category.slug}-#{category.slug}{background-image:url(//awesome.cdn#{background_dark.url})}", - ) - end - - it "applies S3 CDN to background category images" do - setup_s3 - SiteSetting.s3_use_iam_profile = true - SiteSetting.s3_upload_bucket = "test" - SiteSetting.s3_region = "ap-southeast-2" - SiteSetting.s3_cdn_url = "https://s3.cdn" - - background = Fabricate(:upload_s3) - category = Fabricate(:category, uploaded_background: background) - - compiled_css = compile_css("color_definitions") - expect(compiled_css).to include( - "body.category-#{category.slug}{background-image:url(https://s3.cdn/original", - ) - end - - it "applies S3 CDN to dark background category images" do - scheme = ColorScheme.create_from_base(name: "Dark Test", base_scheme_id: "WCAG Dark") - - setup_s3 - SiteSetting.s3_use_iam_profile = true - SiteSetting.s3_upload_bucket = "test" - SiteSetting.s3_region = "ap-southeast-2" - SiteSetting.s3_cdn_url = "https://s3.cdn" - - background = Fabricate(:upload_s3) - background_dark = Fabricate(:upload_s3) - category = - Fabricate( - :category, - uploaded_background: background, - uploaded_background_dark: background_dark, - ) - - compiled_css = compile_css("color_definitions", { color_scheme_id: scheme.id }) - expect(compiled_css).to include( - "body.category-#{category.slug}{background-image:url(https://s3.cdn/original", - ) - end - end - describe "#font" do it "includes font variable" do default_font = ":root{--font-family: Arial, sans-serif}" diff --git a/spec/lib/stylesheet/manager_spec.rb b/spec/lib/stylesheet/manager_spec.rb index d1be670fb17..a53c53b2907 100644 --- a/spec/lib/stylesheet/manager_spec.rb +++ b/spec/lib/stylesheet/manager_spec.rb @@ -483,29 +483,6 @@ RSpec.describe Stylesheet::Manager do describe "color_scheme_digest" do fab!(:theme) - it "changes with category background image" do - category1 = Fabricate(:category, uploaded_background_id: 123, updated_at: 1.week.ago) - category2 = Fabricate(:category, uploaded_background_id: 456, updated_at: 2.days.ago) - - manager = manager(theme.id) - - builder = - Stylesheet::Manager::Builder.new(target: :desktop_theme, theme: theme, manager: manager) - - digest1 = builder.color_scheme_digest - - category2.update!(uploaded_background_id: 789, updated_at: 1.day.ago) - - digest2 = builder.color_scheme_digest - expect(digest2).to_not eq(digest1) - - category1.update!(uploaded_background_id: nil, updated_at: 5.minutes.ago) - - digest3 = builder.color_scheme_digest - expect(digest3).to_not eq(digest2) - expect(digest3).to_not eq(digest1) - end - it "updates digest when updating a color scheme" do scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral") manager = manager(theme.id)