diff --git a/src/api/Settings.ts b/src/api/Settings.ts index b4c2f770..97c603e1 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -183,8 +183,21 @@ export function useSettings(paths?: UseSettings[]) { useEffect(() => { if (paths) { - paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate)); - return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate)); + paths.forEach(p => { + if (p.endsWith(".*")) { + SettingsStore.addPrefixChangeListener(p.slice(0, -2), forceUpdate); + } else { + SettingsStore.addChangeListener(p, forceUpdate); + } + }); + + return () => paths.forEach(p => { + if (p.endsWith(".*")) { + SettingsStore.removePrefixChangeListener(p.slice(0, -2), forceUpdate); + } else { + SettingsStore.removeChangeListener(p, forceUpdate); + } + }); } else { SettingsStore.addGlobalChangeListener(forceUpdate); return () => SettingsStore.removeGlobalChangeListener(forceUpdate); @@ -234,9 +247,11 @@ export function definePluginSettings< if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized"); return PlainSettings.plugins[definedSettings.pluginName] as any; }, - use: settings => useSettings( - settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings[] - ).plugins[definedSettings.pluginName] as any, + use: settings => useSettings(( + settings + ? settings.map(name => `plugins.${definedSettings.pluginName}.${name}`) + : [`plugins.${definedSettings.pluginName}.*`] + ) as UseSettings[]).plugins[definedSettings.pluginName] as any, def, checks: checks ?? {} as any, pluginName: "", @@ -256,7 +271,7 @@ type ResolveUseSettings = { Key extends string ? T[Key] extends Record // @ts-expect-error "Type instantiation is excessively deep and possibly infinite" - ? UseSettings extends string ? `${Key}.${UseSettings}` : never + ? `${Key}.*` | (ResolveUseSettings extends Record ? `${Key}.${ResolveUseSettings[keyof T[Key]]}` : never) : Key : never; }; diff --git a/src/components/settings/tabs/plugins/PluginModal.tsx b/src/components/settings/tabs/plugins/PluginModal.tsx index 76c07177..2a3109b8 100644 --- a/src/components/settings/tabs/plugins/PluginModal.tsx +++ b/src/components/settings/tabs/plugins/PluginModal.tsx @@ -69,7 +69,7 @@ function makeDummyUser(user: { username: string; id?: string; avatar?: string; } } export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) { - const pluginSettings = useSettings().plugins[plugin.name]; + const pluginSettings = useSettings([`plugins.${plugin.name}.*`]).plugins[plugin.name]; const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options)); const [authors, setAuthors] = useState[]>([]); diff --git a/src/components/settings/tabs/vencord/NotificationSettings.tsx b/src/components/settings/tabs/vencord/NotificationSettings.tsx index 2cdaa60e..9024c42b 100644 --- a/src/components/settings/tabs/vencord/NotificationSettings.tsx +++ b/src/components/settings/tabs/vencord/NotificationSettings.tsx @@ -49,7 +49,7 @@ export function openNotificationSettingsModal() { } function NotificationSettings() { - const settings = useSettings().notifications; + const settings = useSettings(["notifications.*"]).notifications; return (
diff --git a/src/shared/SettingsStore.ts b/src/shared/SettingsStore.ts index 1d7dc7f6..5aaf12d0 100644 --- a/src/shared/SettingsStore.ts +++ b/src/shared/SettingsStore.ts @@ -10,7 +10,12 @@ export const SYM_IS_PROXY = Symbol("SettingsStore.isProxy"); export const SYM_GET_RAW_TARGET = Symbol("SettingsStore.getRawTarget"); // Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop -type ResolvePropDeep = P extends `${infer Pre}.${infer Suf}` +type ResolvePropDeep = + P extends `${infer Pre}.*` ? + Pre extends keyof T + ? T[Pre][keyof T[Pre]] + : any + : P extends `${infer Pre}.${infer Suf}` ? Pre extends keyof T ? ResolvePropDeep : any @@ -42,6 +47,7 @@ interface ProxyContext { */ export class SettingsStore { private pathListeners = new Map void>>(); + private prefixListeners = new Map void>>(); private globalListeners = new Set<(newData: T, path: string) => void>(); private readonly proxyContexts = new WeakMap>(); @@ -152,6 +158,13 @@ export class SettingsStore { return new Proxy(object, this.proxyHandler); } + private notifyPrefixListeners(pathString: string, pathElements: string[], value: any) { + for (let i = 1; i <= pathElements.length; i++) { + const prefix = pathElements.slice(0, i).join("."); + this.prefixListeners.get(prefix)?.forEach(cb => cb(value, pathString)); + } + } + private notifyListeners(pathStr: string, value: any, root: T) { const paths = pathStr.split("."); @@ -172,6 +185,7 @@ export class SettingsStore { } this.pathListeners.get(pathStr)?.forEach(cb => cb(value)); + this.notifyPrefixListeners(pathStr, paths, value); } /** @@ -203,6 +217,7 @@ export class SettingsStore { } this.pathListeners.get(pathToNotify)?.forEach(cb => cb(v)); + this.notifyPrefixListeners(pathToNotify, path, v); } this.markAsChanged(); @@ -229,8 +244,6 @@ export class SettingsStore { * ```js * Setting.store.foo.baz = "hi" * ``` - * @param path - * @param cb */ public addChangeListener

>( path: P, @@ -241,6 +254,20 @@ export class SettingsStore { this.pathListeners.set(path as string, listeners); } + /** + * Add a prefix change listener that will fire whenever a setting matching the specified prefix is changed. + * For example if prefix is `"foo"`, the listener will fire on + * ```js + * Setting.store.foo.bar = "hi" + * Setting.store.foo.baz = "hi" + * ``` + */ + public addPrefixChangeListener

(prefix: P, cb: (data: ResolvePropDeep, path: string) => void) { + const listeners = this.prefixListeners.get(prefix) ?? new Set(); + listeners.add(cb); + this.prefixListeners.set(prefix, listeners); + } + /** * Remove a global listener * @see {@link addGlobalChangeListener} @@ -261,6 +288,18 @@ export class SettingsStore { if (!listeners.size) this.pathListeners.delete(path as string); } + /** + * Remove a prefix listener + * @see {@link addPrefixChangeListener} + */ + public removePrefixChangeListener(prefix: string, cb: (data: any, path: string) => void) { + const listeners = this.prefixListeners.get(prefix); + if (!listeners) return; + + listeners.delete(cb); + if (!listeners.size) this.prefixListeners.delete(prefix); + } + /** * Call all global change listeners */