FEATURE: Dark mode alternative logos (#10441)

FEATURE: Dark mode alternative logos (#10441)

diff --git a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js
index 4e44e13..647cd38 100644
--- a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js
+++ b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js
@@ -82,6 +82,20 @@ export default {
       Session.currentProp("safe_mode", setupData.safeMode);
     }
 
+    Session.currentProp(
+      "darkModeAvailable",
+      document.head.querySelectorAll(
+        'link[media="(prefers-color-scheme: dark)"]'
+      ).length > 0
+    );
+
+    Session.currentProp(
+      "darkColorScheme",
+      getComputedStyle(document.documentElement)
+        .getPropertyValue("--scheme-type")
+        .trim() === "dark"
+    );
+
     app.HighlightJSPath = setupData.highlightJsPath;
     Session.currentProp("svgSpritePath", setupData.svgSpritePath);
 
diff --git a/app/assets/javascripts/discourse/app/widgets/home-logo.js b/app/assets/javascripts/discourse/app/widgets/home-logo.js
index 9af8f2b..31a10fd 100644
--- a/app/assets/javascripts/discourse/app/widgets/home-logo.js
+++ b/app/assets/javascripts/discourse/app/widgets/home-logo.js
@@ -4,6 +4,7 @@ import { h } from "virtual-dom";
 import { iconNode } from "discourse-common/lib/icon-library";
 import { wantsNewWindow } from "discourse/lib/intercept-click";
 import DiscourseURL from "discourse/lib/url";
+import Session from "discourse/models/session";
 
 export default createWidget("home-logo", {
   tagName: "div.title",
@@ -17,57 +18,110 @@ export default createWidget("home-logo", {
     return typeof href === "function" ? href() : href;
   },
 
-  logoUrl() {
-    return this.siteSettings.site_logo_url || "";
+  logoUrl(opts = {}) {
+    return this.logoResolver("logo", opts);
   },
 
-  mobileLogoUrl() {
-    return this.siteSettings.site_mobile_logo_url || "";
+  mobileLogoUrl(opts = {}) {
+    return this.logoResolver("mobile_logo", opts);
   },
 
-  smallLogoUrl() {
-    return this.siteSettings.site_logo_small_url || "";
+  smallLogoUrl(opts = {}) {
+    return this.logoResolver("logo_small", opts);
   },
 
   logo() {
-    const { siteSettings } = this;
-    const mobileView = this.site.mobileView;
+    const { siteSettings } = this,
+      mobileView = this.site.mobileView;
+
+    const darkModeOptions = Session.currentProp("darkModeAvailable")
+      ? { dark: true }
+      : {};
+
+    const mobileLogoUrl = this.mobileLogoUrl(),
+      mobileLogoUrlDark = this.mobileLogoUrl(darkModeOptions);
 
-    const mobileLogoUrl = this.mobileLogoUrl();
     const showMobileLogo = mobileView && mobileLogoUrl.length > 0;
 
-    const logoUrl = this.logoUrl();
+    const logoUrl = this.logoUrl(),
+      logoUrlDark = this.logoUrl(darkModeOptions);
     const title = siteSettings.title;
 
     if (this.attrs.minimized) {
-      const logoSmallUrl = this.smallLogoUrl();
+      const logoSmallUrl = this.smallLogoUrl(),
+        logoSmallUrlDark = this.smallLogoUrl(darkModeOptions);
       if (logoSmallUrl.length) {
-        return h("img#site-logo.logo-small", {
-          key: "logo-small",
-          attributes: {
-            src: getURL(logoSmallUrl),
-            width: 36,
-            alt: title
-          }
-        });
+        return this.logoElement(
+          "logo-small",
+          logoSmallUrl,
+          title,
+          logoSmallUrlDark
+        );
       } else {
         return iconNode("home");
       }
     } else if (showMobileLogo) {
-      return h("img#site-logo.logo-big", {
-        key: "logo-mobile",
-        attributes: { src: getURL(mobileLogoUrl), alt: title }
-      });
+      return this.logoElement(
+        "logo-mobile",
+        mobileLogoUrl,
+        title,
+        mobileLogoUrlDark
+      );
     } else if (logoUrl.length) {
-      return h("img#site-logo.logo-big", {
-        key: "logo-big",
-        attributes: { src: getURL(logoUrl), alt: title }
-      });
+      return this.logoElement("logo-big", logoUrl, title, logoUrlDark);
     } else {
       return h("h1#site-text-logo.text-logo", { key: "logo-text" }, title);
     }
   },
 
+  logoResolver(name, opts = {}) {
+    const { siteSettings } = this;
+
+    // get alternative logos for browser dark dark mode switching
+    if (opts.dark) {
+      return siteSettings[`site_${name}_dark_url`];
+    }
+
+    // try dark logos first when color scheme is dark
+    // this is independent of browser dark mode
+    // hence the fallback to normal logos
+    if (Session.currentProp("darkColorScheme")) {
+      return (
+        siteSettings[`site_${name}_dark_url`] ||
+        siteSettings[`site_${name}_url`] ||
+        ""
+      );
+    }
+
+    return siteSettings[`site_${name}_url`] || "";
+  },
+
+  logoElement(key, url, title, darkUrl = null) {
+    const attributes =
+      key === "logo-small"
+        ? { src: getURL(url), width: 36, alt: title }
+        : { src: getURL(url), alt: title };
+
+    const imgElement = h(`img#site-logo.${key}`, {
+      key: key,
+      attributes
+    });
+
+    if (darkUrl && url !== darkUrl) {
+      return h("picture", [
+        h("source", {
+          attributes: {
+            srcset: getURL(darkUrl),
+            media: "(prefers-color-scheme: dark)"
+          }
+        }),
+        imgElement
+      ]);
+    }
+
+    return imgElement;
+  },
+
   html() {
     return h(
       "a",
diff --git a/app/assets/stylesheets/color_definitions.scss b/app/assets/stylesheets/color_definitions.scss
index 047dcd0..72104d8 100644
--- a/app/assets/stylesheets/color_definitions.scss
+++ b/app/assets/stylesheets/color_definitions.scss
@@ -14,7 +14,16 @@
   @return red($hex), green($hex), blue($hex);
 }
 
+@function schemeType() {
+  @if is-light-color-scheme() {
+    @return "light";
+  } @else {
+    @return "dark";
+  }
+}
+
 :root {
+  --scheme-type: #{schemeType()};
   --primary: #{$primary};
   --secondary: #{$secondary};
   --tertiary: #{$tertiary};
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index a4f6f80..daef5fa 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -183,6 +183,9 @@ class SiteSetting < ActiveRecord::Base
     site_logo_small_url
     site_mobile_logo_url
     site_favicon_url
+    site_logo_dark_url
+    site_logo_small_dark_url
+    site_mobile_logo_dark_url
   }.each { |client_setting| client_settings << client_setting }
 
   %i{
@@ -190,6 +193,9 @@ class SiteSetting < ActiveRecord::Base
     logo_small
     digest_logo
     mobile_logo
+    logo_dark
+    logo_small_dark
+    mobile_logo_dark
     large_icon
     manifest_icon
     favicon
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index e2f69d3..3219e53 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1481,6 +1481,9 @@ en:
     logo_small: "The small logo image at the top left of your site, seen when scrolling down. Use a square 120 × 120 image. If left blank, a home glyph will be shown."
     digest_logo: "The alternate logo image used at the top of your site's email summary. Use a wide rectangle image. Don't use an SVG image. If left blank, the image from the `logo` setting will be used."
     mobile_logo: "The logo used on mobile version of your site. Use a wide rectangular image with a height of 120 and an aspect ratio greater than 3:1. If left blank, the image from the `logo` setting will be used."
+    logo_dark: "Dark scheme alternative for the 'logo' site setting."
+    logo_small_dark: "Dark scheme alternative for the 'logo small' site setting."
+    mobile_logo_dark: "Dark scheme alternative for the 'mobile logo' site setting."
     large_icon: "Image used as the base for other metadata icons. Should ideally be larger than 512 x 512. If left blank, logo_small will be used."

[... diff too long, it was truncated ...]

GitHub sha: 3745f2bb

1 Like

This commit appears in #10441 which was approved by eviltrout. It was merged by pmusaraj.

This commit has been mentioned on Discourse Meta. There might be relevant details there:

https://meta.discourse.org/t/pinning-plugin-and-theme-versions-for-older-discourse-installs/156971/15