| 1 | import { personalUiSettingsSyncEnabled, useUiSettingsSync } from "../state/uiSettingsSync"; |
| 2 | import { useAtomValue } from "@effect/atom-react"; |
| 3 | import { useEffect } from "react"; |
| 4 | |
| 5 | import { primaryEnvironmentIdAtom } from "../state/primaryEnvironment"; |
| 6 | import { primaryServerSettingsAtom } from "../state/server"; |
| 7 | import { getThemeDefinition, singleAppearanceOf } from "../themePalette"; |
| 8 | import { useEnvironmentThemeDefinitions } from "./useEnvironmentTheme"; |
| 9 | import { useTheme } from "./useTheme"; |
| 10 | |
| 11 | /** |
| 12 | * Scoped per environment: each machine's `t3 theme set` is its own act, so |
| 13 | * hopping between primary environments neither replays one environment's |
| 14 | * theme over the user's pick nor swallows another's. |
| 15 | */ |
| 16 | const APPLIED_DEFAULT_THEME_STORAGE_PREFIX = "t3code:default-theme-applied:v2:"; |
| 17 | |
| 18 | /** |
| 19 | * One generation per set: keyed on when the theme was set, not just its |
| 20 | * value, so re-asserting the same theme still acts. Environments provisioned |
| 21 | * by builds without the timestamp degrade to applying once per value. |
| 22 | */ |
| 23 | export function defaultThemeGeneration(theme: string, setAt: string): string { |
| 24 | return setAt.length > 0 ? `${theme}@${setAt}` : theme; |
| 25 | } |
| 26 | |
| 27 | /** |
| 28 | * The generation to apply, or null to leave this client alone. Pure so the |
| 29 | * rule -- apply once per set, never replay one already applied, wait for a |
| 30 | * theme that has not arrived yet -- can be tested without a renderer. |
| 31 | */ |
| 32 | export function defaultThemeToApply(input: { |
| 33 | readonly environmentId: string | null; |
| 34 | readonly defaultTheme: string; |
| 35 | readonly defaultThemeSetAt: string; |
| 36 | readonly appliedGeneration: string | null; |
| 37 | readonly resolves: boolean; |
| 38 | readonly profileEnabled: boolean; |
| 39 | }): string | null { |
| 40 | if (input.profileEnabled) return null; |
| 41 | if (input.environmentId === null || input.defaultTheme.length === 0) return null; |
| 42 | const generation = defaultThemeGeneration(input.defaultTheme, input.defaultThemeSetAt); |
| 43 | if (input.appliedGeneration === generation) return null; |
| 44 | // The setting and the palette it names arrive independently, so an id that |
| 45 | // does not resolve yet is not a failure -- the effect runs again when the |
| 46 | // published set changes. |
| 47 | if (!input.resolves) return null; |
| 48 | return generation; |
| 49 | } |
| 50 | |
| 51 | function readAppliedGeneration(storageKey: string): string | null { |
| 52 | if (typeof window === "undefined") return null; |
| 53 | try { |
| 54 | return window.localStorage.getItem(storageKey); |
| 55 | } catch { |
| 56 | return null; |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | function writeAppliedGeneration(storageKey: string, generation: string): void { |
| 61 | try { |
| 62 | window.localStorage.setItem(storageKey, generation); |
| 63 | } catch { |
| 64 | // Unrecordable means the next config event applies again; harmless. |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * Applies the environment's theme (`t3 theme set <id>`). Each set switches |
| 70 | * this client once — live when connected, on the next connect otherwise — |
| 71 | * and then steps aside: a theme the user picks in Settings afterwards wins |
| 72 | * until the environment's theme is set again. The environment's own published |
| 73 | * themes are valid targets, which is why this waits for an id that does not |
| 74 | * resolve yet — the setting and the palette it names arrive independently. |
| 75 | */ |
| 76 | export function useDefaultThemeAdoption(): void { |
| 77 | const sync = useUiSettingsSync(); |
| 78 | const profileEnabled = sync.controller !== null || personalUiSettingsSyncEnabled(); |
| 79 | const environmentId = useAtomValue(primaryEnvironmentIdAtom); |
| 80 | const settings = useAtomValue(primaryServerSettingsAtom); |
| 81 | const { defaultTheme, defaultThemeSetAt } = settings; |
| 82 | const { setTheme, setAppearanceMode } = useTheme(); |
| 83 | // Re-runs adoption when a late-arriving published theme makes the |
| 84 | // requested id resolvable. |
| 85 | const environmentThemes = useEnvironmentThemeDefinitions(); |
| 86 | |
| 87 | useEffect(() => { |
| 88 | if (typeof window === "undefined" || environmentId === null) return; |
| 89 | const storageKey = `${APPLIED_DEFAULT_THEME_STORAGE_PREFIX}${environmentId}`; |
| 90 | if (profileEnabled) { |
| 91 | if (defaultTheme.length > 0) |
| 92 | writeAppliedGeneration(storageKey, defaultThemeGeneration(defaultTheme, defaultThemeSetAt)); |
| 93 | } |
| 94 | const definition = getThemeDefinition(defaultTheme); |
| 95 | const generation = defaultThemeToApply({ |
| 96 | environmentId, |
| 97 | defaultTheme, |
| 98 | defaultThemeSetAt, |
| 99 | appliedGeneration: readAppliedGeneration(storageKey), |
| 100 | resolves: definition !== null, |
| 101 | profileEnabled, |
| 102 | }); |
| 103 | if (generation === null || definition === null) return; |
| 104 | |
| 105 | // Deliberately not the card's rule. Clicking a card is a user choosing one |
| 106 | // half of their own mix; a set theme is the environment saying what this |
| 107 | // client opens on, so it takes the base preference and, for a |
| 108 | // single-appearance theme, the matching mode -- otherwise a dark-only |
| 109 | // theme on a light client is recorded as applied while nothing changes. |
| 110 | if (!setTheme(defaultTheme)) return; |
| 111 | const half = singleAppearanceOf(definition); |
| 112 | // Recorded only once both land: marking the generation applied while the |
| 113 | // appearance switch failed would leave a dark-only theme rendering its |
| 114 | // light half with no retry. |
| 115 | if (half !== null && !setAppearanceMode(half)) return; |
| 116 | writeAppliedGeneration(storageKey, generation); |
| 117 | }, [ |
| 118 | profileEnabled, |
| 119 | environmentId, |
| 120 | defaultTheme, |
| 121 | defaultThemeSetAt, |
| 122 | environmentThemes, |
| 123 | setTheme, |
| 124 | setAppearanceMode, |
| 125 | ]); |
| 126 | } |