diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx index 49b527f0..263d939d 100644 --- a/src/context/Theme.tsx +++ b/src/context/Theme.tsx @@ -4,11 +4,11 @@ import { createGlobalStyle } from "styled-components"; import { createContext } from "preact"; import { useEffect } from "preact/hooks"; +import { useApplicationState } from "../mobx/State"; +import { getState } from "../redux"; import { connectState } from "../redux/connector"; import { Children } from "../types/Preact"; -import { fetchManifest, fetchTheme } from "../pages/settings/panes/ThemeShop"; -import { getState } from "../redux"; export type Variables = | "accent" @@ -57,6 +57,7 @@ export type Fonts = | "Raleway" | "Ubuntu" | "Comic Neue"; + export type MonospaceFonts = | "Fira Code" | "Roboto Mono" @@ -285,23 +286,23 @@ export const PRESETS: Record = { // todo: store used themes locally export function getBaseTheme(name: string): Theme { if (name in PRESETS) { - return PRESETS[name] + return PRESETS[name]; } // TODO: properly initialize `themes` in state instead of letting it be undefined - const themes = getState().themes ?? {} + const themes = getState().themes ?? {}; if (name in themes) { const { theme } = themes[name]; return { - ...PRESETS[theme.light ? 'light' : 'dark'], - ...theme - } + ...PRESETS[theme.light ? "light" : "dark"], + ...theme, + }; } // how did we get here - return PRESETS['dark'] + return PRESETS["dark"]; } const keys = Object.keys(PRESETS.dark); @@ -315,21 +316,22 @@ export const generateVariables = (theme: Theme) => { return (Object.keys(theme) as Variables[]).map((key) => { if (!keys.includes(key)) return; return `--${key}: ${theme[key]};`; - }) -} + }); +}; // Load the default default them and apply extras later export const ThemeContext = createContext(PRESETS["dark"]); interface Props { children: Children; - options?: ThemeOptions; } -function Theme({ children, options }: Props) { +export default function Theme({ children }: Props) { + const settings = useApplicationState().settings; + const theme: Theme = { - ...getBaseTheme(options?.base ?? 'dark'), - ...options?.custom, + ...getBaseTheme(settings.get("appearance:theme:base") ?? "dark"), + ...settings.get("appearance:theme:custom"), }; const root = document.documentElement.style; @@ -346,8 +348,11 @@ function Theme({ children, options }: Props) { }, [root, theme.monospaceFont]); useEffect(() => { - root.setProperty("--ligatures", options?.ligatures ? "normal" : "none"); - }, [root, options?.ligatures]); + root.setProperty( + "--ligatures", + settings.get("appearance:ligatures") ? "normal" : "none", + ); + }, [root, settings.get("appearance:ligatures")]); useEffect(() => { const resize = () => @@ -371,9 +376,3 @@ function Theme({ children, options }: Props) { ); } - -export default connectState<{ children: Children }>(Theme, (state) => { - return { - options: state.settings.theme, - }; -}); diff --git a/src/mobx/State.ts b/src/mobx/State.ts index 041ebb2e..9c94cee3 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -13,6 +13,7 @@ import LocaleOptions from "./stores/LocaleOptions"; import MessageQueue from "./stores/MessageQueue"; import NotificationOptions from "./stores/NotificationOptions"; import ServerConfig from "./stores/ServerConfig"; +import Settings from "./stores/Settings"; /** * Handles global application state. @@ -26,6 +27,7 @@ export default class State { config: ServerConfig; notifications: NotificationOptions; queue: MessageQueue; + settings: Settings; private persistent: [string, Persistent][] = []; @@ -41,6 +43,7 @@ export default class State { this.config = new ServerConfig(); this.notifications = new NotificationOptions(); this.queue = new MessageQueue(); + this.settings = new Settings(); makeAutoObservable(this); this.registerListeners = this.registerListeners.bind(this); diff --git a/src/mobx/stores/Settings.ts b/src/mobx/stores/Settings.ts new file mode 100644 index 00000000..9828becd --- /dev/null +++ b/src/mobx/stores/Settings.ts @@ -0,0 +1,87 @@ +import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; + +import { mapToRecord } from "../../lib/conversion"; + +import { Theme } from "../../context/Theme"; + +import { Sounds } from "../../assets/sounds/Audio"; +import Persistent from "../interfaces/Persistent"; +import Store from "../interfaces/Store"; + +export type SoundOptions = { + [key in Sounds]?: boolean; +}; + +export type EmojiPack = "mutant" | "twemoji" | "noto" | "openmoji"; + +interface ISettings { + "notifications:desktop": boolean; + "notifications:sounds": SoundOptions; + + "appearance:emoji": EmojiPack; + "appearance:ligatures": boolean; + "appearance:theme:base": string; + "appearance:theme:custom": Partial; +} + +/*const Schema: { + [key in keyof ISettings]: + | "string" + | "number" + | "boolean" + | "object" + | "function"; +} = { + "notifications:desktop": "boolean", + "notifications:sounds": "object", + + "appearance:emoji": "string", + "appearance:ligatures": "boolean", + "appearance:theme:base": "string", + "appearance:theme:custom": "object", +};*/ + +/** + * Manages user settings. + */ +export default class Settings implements Store, Persistent { + private data: ObservableMap; + + /** + * Construct new Layout store. + */ + constructor() { + this.data = new ObservableMap(); + makeAutoObservable(this); + } + + get id() { + return "layout"; + } + + toJSON() { + return JSON.parse(JSON.stringify(mapToRecord(this.data))); + } + + @action hydrate(data: ISettings) { + Object.keys(data).forEach((key) => + this.data.set(key, (data as any)[key]), + ); + } + + @action set(key: T, value: ISettings[T]) { + return this.data.set(key, value); + } + + @computed get(key: T) { + return this.data.get(key) as ISettings[T] | undefined; + } + + @action setUnchecked(key: string, value: unknown) { + return this.data.set(key, value); + } + + @computed getUnchecked(key: string) { + return this.data.get(key); + } +}