Преглед на файлове

Sync Reddit web theme with app dark mode and hide broken Google login.

Reddit now follows the Settings appearance toggle via web view styling and injected theme sync, and Google sign-in is removed from auth surfaces where OAuth fails in WKWebView.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 преди 1 месец
родител
ревизия
65413430c0

+ 26 - 3
Reddit App/Managers/RedditWebViewManager.swift

@@ -12,6 +12,14 @@ enum RedditWebViewManager {
         configuration.defaultWebpagePreferences.allowsContentJavaScript = true
         configuration.preferences.isElementFullscreenEnabled = true
 
+        let isDarkMode = AppearanceManager.shared.isDarkMode
+        let themeScript = WKUserScript(
+            source: RedditThemeStyle.earlyInjectionScript(isDarkMode: isDarkMode),
+            injectionTime: .atDocumentStart,
+            forMainFrameOnly: true
+        )
+        configuration.userContentController.addUserScript(themeScript)
+
         let boundaryScript = WKUserScript(
             source: RedditPostBoundaryStyle.injectionScript,
             injectionTime: .atDocumentEnd,
@@ -28,8 +36,8 @@ enum RedditWebViewManager {
 
         let authChromeScript = WKUserScript(
             source: RedditAuthChromeStyle.injectionScript,
-            injectionTime: .atDocumentEnd,
-            forMainFrameOnly: true
+            injectionTime: .atDocumentStart,
+            forMainFrameOnly: false
         )
         configuration.userContentController.addUserScript(authChromeScript)
 
@@ -54,10 +62,25 @@ enum RedditWebViewManager {
         webView.allowsBackForwardNavigationGestures = true
         webView.allowsMagnification = true
         webView.customUserAgent = RedditWebAuthHelper.safariUserAgent
-        applyUnderPageBackground(to: webView, isDarkMode: AppearanceManager.shared.isDarkMode)
+        let isDarkMode = AppearanceManager.shared.isDarkMode
+        applyTheme(to: webView, isDarkMode: isDarkMode)
         return webView
     }
 
+    static func applyTheme(to webView: WKWebView, isDarkMode: Bool) {
+        applyAppearance(to: webView, isDarkMode: isDarkMode)
+        applyUnderPageBackground(to: webView, isDarkMode: isDarkMode)
+        webView.evaluateJavaScript(
+            RedditThemeStyle.syncScript(isDarkMode: isDarkMode),
+            completionHandler: nil
+        )
+    }
+
+    static func applyAppearance(to webView: WKWebView, isDarkMode: Bool) {
+        let appearanceName: NSAppearance.Name = isDarkMode ? .darkAqua : .aqua
+        webView.appearance = NSAppearance(named: appearanceName)
+    }
+
     static func applyUnderPageBackground(to webView: WKWebView, isDarkMode: Bool) {
         webView.underPageBackgroundColor = AppearanceManager.webUnderPageBackgroundColor(isDarkMode: isDarkMode)
     }

+ 91 - 4
Reddit App/Utilities/RedditAuthChromeStyle.swift

@@ -1,12 +1,99 @@
 import Foundation
 
 enum RedditAuthChromeStyle {
-    /// Placeholder for optional Reddit auth UI tweaks. Close buttons are left visible
-    /// so users can dismiss the login modal if sign-in fails or stalls.
+    /// Hides Google sign-in on Reddit auth surfaces. Google OAuth does not work inside
+    /// WKWebView subframes on macOS, so we remove the option instead of showing a broken flow.
     static let injectionScript = """
     (function() {
-      const STYLE_ID = 'reddora-auth-chrome-style';
-      if (document.getElementById(STYLE_ID)) return;
+      if (window.__reddoraGoogleLoginHidden) return;
+      window.__reddoraGoogleLoginHidden = true;
+
+      const GOOGLE_PATTERNS = [
+        /continue with google/i,
+        /sign in with google/i,
+        /sign in with google account/i
+      ];
+      const SELECTORS = 'button, a, [role="button"], faceplate-tracker, faceplate-button';
+
+      function buttonLabel(node) {
+        return (
+          (node.textContent || '') + ' ' +
+          (node.getAttribute('aria-label') || '') + ' ' +
+          (node.getAttribute('title') || '')
+        ).trim();
+      }
+
+      function isGoogleLoginButton(node) {
+        return GOOGLE_PATTERNS.some(function(pattern) {
+          return pattern.test(buttonLabel(node));
+        });
+      }
+
+      function hideElement(node) {
+        const target = node.closest('faceplate-tracker, faceplate-button') || node;
+        target.style.setProperty('display', 'none', 'important');
+        target.style.setProperty('visibility', 'hidden', 'important');
+        target.setAttribute('aria-hidden', 'true');
+        target.setAttribute('hidden', '');
+      }
+
+      function collectClickables(root, results) {
+        if (!root) return;
+
+        if (root.querySelectorAll) {
+          root.querySelectorAll(SELECTORS).forEach(function(node) {
+            results.push(node);
+          });
+        }
+
+        const children = root.children || root.childNodes;
+        for (let i = 0; i < children.length; i++) {
+          const child = children[i];
+          if (!(child instanceof Element)) continue;
+          if (child.shadowRoot) {
+            collectClickables(child.shadowRoot, results);
+          }
+          collectClickables(child, results);
+        }
+      }
+
+      function hideGoogleLoginButtons(root) {
+        const nodes = [];
+        collectClickables(root || document.documentElement, nodes);
+
+        for (let i = 0; i < nodes.length; i++) {
+          const node = nodes[i];
+          if (!isGoogleLoginButton(node)) continue;
+          hideElement(node);
+        }
+      }
+
+      document.addEventListener('click', function(event) {
+        const path = event.composedPath ? event.composedPath() : [event.target];
+        for (let i = 0; i < path.length; i++) {
+          const node = path[i];
+          if (!(node instanceof Element)) continue;
+          if (!node.matches || !node.matches(SELECTORS)) continue;
+          if (!isGoogleLoginButton(node)) continue;
+
+          event.preventDefault();
+          event.stopImmediatePropagation();
+          return;
+        }
+      }, true);
+
+      hideGoogleLoginButtons(document);
+
+      const observer = new MutationObserver(function() {
+        hideGoogleLoginButtons(document);
+      });
+      observer.observe(document.documentElement, { childList: true, subtree: true });
+
+      [250, 750, 1500, 3000].forEach(function(delay) {
+        setTimeout(function() {
+          hideGoogleLoginButtons(document);
+        }, delay);
+      });
     })();
     """
 }

+ 147 - 0
Reddit App/Utilities/RedditThemeStyle.swift

@@ -0,0 +1,147 @@
+import Foundation
+
+enum RedditThemeStyle {
+    static func earlyInjectionScript(isDarkMode: Bool) -> String {
+        """
+        (function() {
+          const wantDark = \(isDarkMode ? "true" : "false");
+          window.__reddoraTheme = { wantsDark: wantDark };
+
+          const installMatchMediaOverride = (wantsDark) => {
+            if (window.__reddoraTheme.matchMediaInstalled) return;
+            const original = window.matchMedia.bind(window);
+            window.matchMedia = function(query) {
+              const result = original(query);
+              if (typeof query === 'string' && query.includes('prefers-color-scheme')) {
+                const matches = query.includes('dark') ? wantsDark : !wantsDark;
+                return {
+                  matches,
+                  media: query,
+                  onchange: null,
+                  addEventListener: (...args) => result.addEventListener(...args),
+                  removeEventListener: (...args) => result.removeEventListener(...args),
+                  dispatchEvent: (...args) => result.dispatchEvent(...args),
+                };
+              }
+              return result;
+            };
+            window.__reddoraTheme.matchMediaInstalled = true;
+          };
+
+          installMatchMediaOverride(wantDark);
+        })();
+        """
+    }
+
+    static func syncScript(isDarkMode: Bool) -> String {
+        """
+        (function() {
+          const wantDark = \(isDarkMode ? "true" : "false");
+          window.__reddoraTheme = window.__reddoraTheme || {};
+          window.__reddoraTheme.wantsDark = wantDark;
+
+          const installMatchMediaOverride = (wantsDark) => {
+            const original = window.matchMedia.bind(window);
+            window.matchMedia = function(query) {
+              const result = original(query);
+              if (typeof query === 'string' && query.includes('prefers-color-scheme')) {
+                const matches = query.includes('dark') ? wantsDark : !wantsDark;
+                return {
+                  matches,
+                  media: query,
+                  onchange: null,
+                  addEventListener: (...args) => result.addEventListener(...args),
+                  removeEventListener: (...args) => result.removeEventListener(...args),
+                  dispatchEvent: (...args) => result.dispatchEvent(...args),
+                };
+              }
+              return result;
+            };
+            window.__reddoraTheme.matchMediaInstalled = true;
+          };
+
+          installMatchMediaOverride(wantDark);
+
+          const isDrawerHidden = (element) => !element || (element.offsetWidth === 0 && element.offsetHeight === 0);
+
+          const isRedditDarkModeActive = () => {
+            const header = document.querySelector('header');
+            const themedNode = header?.firstElementChild;
+            if (themedNode) {
+              const background = getComputedStyle(themedNode)
+                .getPropertyValue('--shreddit-content-background')
+                .trim()
+                .toLowerCase();
+              if (background === '#0e1113' || background === '#1a1a1b') return true;
+              if (background === '#ffffff' || background === '#fff') return false;
+            }
+
+            const root = document.documentElement;
+            if (root.classList.contains('theme-dark')) return true;
+            if (root.classList.contains('theme-light')) return false;
+
+            return window.matchMedia('(prefers-color-scheme: dark)').matches;
+          };
+
+          const toggleViaDrawer = () => {
+            const drawerButton = document.querySelector('#expand-user-drawer-button');
+            const drawer = document.querySelector('#user-drawer-content');
+            const switchInput = document.querySelector(
+              '#darkmode-list-item faceplate-switch-input[name="darkmode-switch-name"]'
+            );
+            if (!drawerButton || !drawer || !switchInput) return false;
+
+            const drawerWasHidden = isDrawerHidden(drawer);
+            if (drawerWasHidden) drawerButton.click();
+
+            const isChecked = switchInput.checked === true
+              || switchInput.getAttribute('checked') !== null
+              || switchInput.getAttribute('aria-checked') === 'true';
+            if (isChecked !== wantDark) {
+              switchInput.click();
+            }
+
+            if (drawerWasHidden) {
+              setTimeout(() => {
+                if (!isDrawerHidden(drawer)) drawerButton.click();
+              }, 250);
+            }
+
+            return true;
+          };
+
+          const syncViaAPI = async () => {
+            try {
+              const response = await fetch('/api/v1/me/prefs', {
+                method: 'PATCH',
+                headers: { 'Content-Type': 'application/json' },
+                credentials: 'same-origin',
+                body: JSON.stringify({ nightmode: wantDark }),
+              });
+              return response.ok;
+            } catch {
+              return false;
+            }
+          };
+
+          const run = async () => {
+            const current = isRedditDarkModeActive();
+            if (current === wantDark) return;
+
+            if (toggleViaDrawer()) return;
+
+            const updated = await syncViaAPI();
+            if (updated) {
+              window.location.reload();
+            }
+          };
+
+          if (document.readyState === 'loading') {
+            document.addEventListener('DOMContentLoaded', () => run(), { once: true });
+          } else {
+            run();
+          }
+        })();
+        """
+    }
+}

+ 9 - 4
Reddit App/Views/RedditWebView.swift

@@ -26,7 +26,7 @@ struct RedditWebView: NSViewRepresentable {
         context.coordinator.lastAppliedDarkMode = isDarkMode
         context.coordinator.installLoginBridge()
         context.coordinator.startObservingNavigation(in: container.webView)
-        container.applyUnderPageBackground(isDarkMode: isDarkMode)
+        container.applyTheme(isDarkMode: isDarkMode)
         container.load(url: url)
         return container
     }
@@ -37,7 +37,7 @@ struct RedditWebView: NSViewRepresentable {
 
         if context.coordinator.lastAppliedDarkMode != isDarkMode {
             context.coordinator.lastAppliedDarkMode = isDarkMode
-            container.applyUnderPageBackground(isDarkMode: isDarkMode)
+            container.applyTheme(isDarkMode: isDarkMode)
         }
 
         if context.coordinator.lastGoBackTrigger != goBackTrigger {
@@ -109,6 +109,10 @@ struct RedditWebView: NSViewRepresentable {
             webView.evaluateJavaScript(RedditPostBoundaryStyle.injectionScript, completionHandler: nil)
             webView.evaluateJavaScript(RedditHomeChromeStyle.injectionScript, completionHandler: nil)
             webView.evaluateJavaScript(RedditAuthChromeStyle.injectionScript, completionHandler: nil)
+            webView.evaluateJavaScript(
+                RedditThemeStyle.syncScript(isDarkMode: lastAppliedDarkMode ?? AppearanceManager.shared.isDarkMode),
+                completionHandler: nil
+            )
             if let url = webView.url, !RedditWebAuthHelper.isCredentialLoginPage(url) {
                 webView.evaluateJavaScript(RedditLoginBridgeStyle.injectionScript, completionHandler: nil)
             }
@@ -340,9 +344,10 @@ final class RedditWebViewContainer: NSView {
         webView.load(URLRequest(url: url))
     }
 
-    func applyUnderPageBackground(isDarkMode: Bool) {
+    func applyTheme(isDarkMode: Bool) {
         let color = AppearanceManager.webUnderPageBackgroundColor(isDarkMode: isDarkMode)
-        RedditWebViewManager.applyUnderPageBackground(to: webView, isDarkMode: isDarkMode)
+        RedditWebViewManager.applyTheme(to: webView, isDarkMode: isDarkMode)
+        appearance = NSAppearance(named: isDarkMode ? .darkAqua : .aqua)
         layer?.backgroundColor = color.cgColor
     }
 }