Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 59 additions & 16 deletions apps/web/core/lib/wrappers/store-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { useTheme } from "next-themes";
Expand Down Expand Up @@ -28,6 +28,13 @@ function StoreWrapper(props: TStoreWrapper) {
const { data: userProfile } = useUserProfile();
const { changeLanguage } = useTranslation();

// Track if we've initialized theme from server (one-time only)
const hasInitializedThemeRef = useRef(false);
// Track current user to reset on logout/login
const currentUserIdRef = useRef<string | undefined>(undefined);
// Track previous theme to detect transitions from custom theme
const previousThemeRef = useRef<string | undefined>(undefined);

/**
* Sidebar collapsed fetching from local storage
*/
Expand All @@ -38,25 +45,61 @@ function StoreWrapper(props: TStoreWrapper) {
}, [sidebarCollapsed, setTheme, toggleSidebar]);

/**
* Setting up the theme of the user by fetching it from profile
* Effect 1: Initial theme sync from server (one-time only)
*
* This effect runs ONCE per user session to load theme from server.
* After initial load, all theme changes are localStorage-driven (next-themes).
* This prevents a feedback loop where server updates trigger UI updates in a cycle.
*/
useEffect(() => {
const userId = userProfile?.id;

// Reset initialization flag when user changes (logout/login)
// This handles both logout (userId becomes undefined) and login (userId changes)
if (userId !== currentUserIdRef.current) {
hasInitializedThemeRef.current = false;
previousThemeRef.current = undefined;
currentUserIdRef.current = userId;
}

// Only initialize theme from server on FIRST load for this user
if (!userProfile?.theme?.theme || hasInitializedThemeRef.current) {
return; // Skip if already initialized or no profile data
}

// Apply theme from server profile (one-time only)
setTheme(userProfile?.theme?.theme || "system");

// Mark as initialized - prevents future syncs from server
hasInitializedThemeRef.current = true;
}, [userProfile?.theme?.theme, setTheme]);

/**
* Effect 2: Custom theme CSS application (runs on every change)
*
* This effect applies or clears custom theme CSS variables whenever
* the theme changes. It runs independently of the initial sync effect.
*/
useEffect(() => {
if (!userProfile?.theme?.theme) return;
const currentTheme = userProfile?.theme?.theme || "system";
const theme = userProfile?.theme;

if (currentTheme) {
setTheme(currentTheme);
if (currentTheme === "custom") {
// New 2-color palette system
if (theme.primary && theme.background && theme.darkPalette !== undefined) {
applyCustomTheme(theme.primary, theme.background, theme.darkPalette ? "dark" : "light");
}
} else {
clearCustomTheme();
}

const currentTheme = userProfile?.theme?.theme;
const previousTheme = previousThemeRef.current;
const themeData = userProfile?.theme;

// Apply custom theme if current theme is custom
if (currentTheme === "custom" && themeData.primary && themeData.background && themeData.darkPalette !== undefined) {
applyCustomTheme(themeData.primary, themeData.background, themeData.darkPalette ? "dark" : "light");
}
// Clear custom theme CSS when switching away from custom
else if (previousTheme === "custom" && currentTheme !== "custom") {
clearCustomTheme();
// No reload needed - let CSS cascade handle it naturally
}
}, [userProfile?.theme, setTheme]);

// Update previous theme for next comparison
previousThemeRef.current = currentTheme;
}, [userProfile?.theme]);

useEffect(() => {
if (!userProfile?.language) return;
Expand Down
Loading