Skip to content
Source
Sign inSign up

useDefaultTheme.ts

86 lines3.4 KB

1import { appAtomRegistry } from "../rpc/atomRegistry";
2import { themeEnvironmentIdAtom } from "../state/primaryEnvironment";
3import { themeServerSettingsAtom } from "../state/server";
4import { getThemeDefinition, singleAppearanceOf, subscribeToCustomThemes } from "../themePalette";
5import { setAppearanceMode, setTheme } from "./useTheme";
6​
7const APPLIED_DEFAULT_THEME_STORAGE_PREFIX = "t3code:default-theme-applied:v2:";
8​
9/** Include set time so reasserting the same theme still applies. */
10export function defaultThemeGeneration(theme: string, setAt: string): string {
11 return setAt.length > 0 ? `${theme}@${setAt}` : theme;
13​
14export function defaultThemeToApply(input: {
15 readonly environmentId: string | null;
16 readonly defaultTheme: string;
17 readonly defaultThemeSetAt: string;
18 readonly appliedGeneration: string | null;
19 readonly resolves: boolean;
20}): string | null {
21 if (input.environmentId === null || input.defaultTheme.length === 0) return null;
22 const generation = defaultThemeGeneration(input.defaultTheme, input.defaultThemeSetAt);
23 if (input.appliedGeneration === generation) return null;
24 // The setting and the palette it names arrive independently, so an id that
25 // does not resolve yet is not a failure -- adoption re-checks when the
26 // published set changes.
27 if (!input.resolves) return null;
28 return generation;
30​
31function readAppliedGeneration(storageKey: string): string | null {
32 if (typeof window === "undefined") return null;
33 try {
34 return window.localStorage.getItem(storageKey);
35 } catch {
36 return null;
37 }
39​
40function writeAppliedGeneration(storageKey: string, generation: string): void {
41 try {
42 window.localStorage.setItem(storageKey, generation);
43 } catch {}
45​
46function adoptDefaultTheme(): void {
47 if (typeof window === "undefined") return;
48 const environmentId = appAtomRegistry.get(themeEnvironmentIdAtom);
49 if (environmentId === null) return;
50 const { defaultTheme, defaultThemeSetAt } = appAtomRegistry.get(themeServerSettingsAtom);
51 const definition = getThemeDefinition(defaultTheme);
52 const storageKey = `${APPLIED_DEFAULT_THEME_STORAGE_PREFIX}${environmentId}`;
53 const generation = defaultThemeToApply({
54 environmentId,
55 defaultTheme,
56 defaultThemeSetAt,
57 appliedGeneration: readAppliedGeneration(storageKey),
58 resolves: definition !== null,
59 });
60 if (generation === null || definition === null) return;
61​
62 // A single-appearance theme needs both preference and appearance writes to take effect.
63 if (!setTheme(defaultTheme)) return;
64 const half = singleAppearanceOf(definition);
65 // Only record adoption after both writes succeed, so an appearance failure remains retryable.
66 if (half !== null && !setAppearanceMode(half)) return;
67 writeAppliedGeneration(storageKey, generation);
69​
70/**
71 * Applies the theme environment's default theme once per generation. Start it after the
72 * environment theme sync so a first-run client adopting the environment's own theme finds it
73 * already in the library; it re-checks when published or custom palettes change, so a theme
74 * that resolves late still applies.
75 */
76export function startDefaultThemeAdoption(): () => void {
77 const unsubscribes = [
78 appAtomRegistry.subscribe(themeEnvironmentIdAtom, adoptDefaultTheme),
79 appAtomRegistry.subscribe(themeServerSettingsAtom, adoptDefaultTheme),
80 subscribeToCustomThemes(adoptDefaultTheme),
81 ];
82 adoptDefaultTheme();
83 return () => {
84 for (const unsubscribe of unsubscribes) unsubscribe();
85 };