}
/>
diff --git a/src/api/Settings.ts b/src/api/Settings.ts
index 004a8988b..70ba0bd4a 100644
--- a/src/api/Settings.ts
+++ b/src/api/Settings.ts
@@ -16,10 +16,11 @@
* along with this program. If not, see .
*/
-import { debounce } from "@utils/debounce";
+import { debounce } from "@shared/debounce";
+import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
import { localStorage } from "@utils/localStorage";
import { Logger } from "@utils/Logger";
-import { mergeDefaults } from "@utils/misc";
+import { mergeDefaults } from "@utils/mergeDefaults";
import { putCloudSettings } from "@utils/settingsSync";
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
import { React } from "@webpack/common";
@@ -28,7 +29,6 @@ import plugins from "~plugins";
const logger = new Logger("Settings");
export interface Settings {
- notifyAboutUpdates: boolean;
autoUpdate: boolean;
autoUpdateNotification: boolean,
useQuickCss: boolean;
@@ -52,7 +52,6 @@ export interface Settings {
| "under-page"
| "window"
| undefined;
- macosTranslucency: boolean | undefined;
disableMinSize: boolean;
winNativeTitleBar: boolean;
plugins: {
@@ -78,8 +77,7 @@ export interface Settings {
}
const DefaultSettings: Settings = {
- notifyAboutUpdates: true,
- autoUpdate: false,
+ autoUpdate: true,
autoUpdateNotification: true,
useQuickCss: true,
themeLinks: [],
@@ -88,8 +86,6 @@ const DefaultSettings: Settings = {
frameless: false,
transparent: false,
winCtrlQ: false,
- // Replaced by macosVibrancyStyle
- macosTranslucency: undefined,
macosVibrancyStyle: undefined,
disableMinSize: false,
winNativeTitleBar: false,
@@ -110,13 +106,8 @@ const DefaultSettings: Settings = {
}
};
-try {
- var settings = JSON.parse(VencordNative.settings.get()) as Settings;
- mergeDefaults(settings, DefaultSettings);
-} catch (err) {
- var settings = mergeDefaults({} as Settings, DefaultSettings);
- logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
-}
+const settings = !IS_REPORTER ? VencordNative.settings.get() : {} as Settings;
+mergeDefaults(settings, DefaultSettings);
const saveSettingsOnFrequentAction = debounce(async () => {
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
@@ -125,74 +116,52 @@ const saveSettingsOnFrequentAction = debounce(async () => {
}
}, 60_000);
-type SubscriptionCallback = ((newValue: any, path: string) => void) & { _paths?: Array; };
-const subscriptions = new Set();
-const proxyCache = {} as Record;
+export const SettingsStore = new SettingsStoreClass(settings, {
+ readOnly: true,
+ getDefaultValue({
+ target,
+ key,
+ path
+ }) {
+ const v = target[key];
+ if (!plugins) return v; // plugins not initialised yet. this means this path was reached by being called on the top level
-// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
-function makeProxy(settings: any, root = settings, path = ""): Settings {
- return proxyCache[path] ??= new Proxy(settings, {
- get(target, p: string) {
- const v = target[p];
+ if (path === "plugins" && key in plugins)
+ return target[key] = {
+ enabled: IS_REPORTER ?? plugins[key].required ?? plugins[key].enabledByDefault ?? false
+ };
- // using "in" is important in the following cases to properly handle falsy or nullish values
- if (!(p in target)) {
- // Return empty for plugins with no settings
- if (path === "plugins" && p in plugins)
- return target[p] = makeProxy({
- enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
- }, root, `plugins.${p}`);
+ // Since the property is not set, check if this is a plugin's setting and if so, try to resolve
+ // the default value.
+ if (path.startsWith("plugins.")) {
+ const plugin = path.slice("plugins.".length);
+ if (plugin in plugins) {
+ const setting = plugins[plugin].options?.[key];
+ if (!setting) return v;
- // Since the property is not set, check if this is a plugin's setting and if so, try to resolve
- // the default value.
- if (path.startsWith("plugins.")) {
- const plugin = path.slice("plugins.".length);
- if (plugin in plugins) {
- const setting = plugins[plugin].options?.[p];
- if (!setting) return v;
- if ("default" in setting)
- // normal setting with a default value
- return (target[p] = setting.default);
- if (setting.type === OptionType.SELECT) {
- const def = setting.options.find(o => o.default);
- if (def)
- target[p] = def.value;
- return def?.value;
- }
- }
- }
- return v;
- }
+ if ("default" in setting)
+ // normal setting with a default value
+ return (target[key] = setting.default);
- // Recursively proxy Objects with the updated property path
- if (typeof v === "object" && !Array.isArray(v) && v !== null)
- return makeProxy(v, root, `${path}${path && "."}${p}`);
-
- // primitive or similar, no need to proxy further
- return v;
- },
-
- set(target, p: string, v) {
- // avoid unnecessary updates to React Components and other listeners
- if (target[p] === v) return true;
-
- target[p] = v;
- // Call any listeners that are listening to a setting of this path
- const setPath = `${path}${path && "."}${p}`;
- delete proxyCache[setPath];
- for (const subscription of subscriptions) {
- if (!subscription._paths || subscription._paths.includes(setPath)) {
- subscription(v, setPath);
+ if (setting.type === OptionType.SELECT) {
+ const def = setting.options.find(o => o.default);
+ if (def)
+ target[key] = def.value;
+ return def?.value;
}
}
- // And don't forget to persist the settings!
- PlainSettings.cloud.settingsSyncVersion = Date.now();
- localStorage.Vencord_settingsDirty = true;
- saveSettingsOnFrequentAction();
- VencordNative.settings.set(JSON.stringify(root, null, 4));
- return true;
}
+ return v;
+ }
+});
+
+if (!IS_REPORTER) {
+ SettingsStore.addGlobalChangeListener((_, path) => {
+ SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
+ localStorage.Vencord_settingsDirty = true;
+ saveSettingsOnFrequentAction();
+ VencordNative.settings.set(SettingsStore.plain, path);
});
}
@@ -210,7 +179,7 @@ export const PlainSettings = settings;
* the updated settings to disk.
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
*/
-export const Settings = makeProxy(settings);
+export const Settings = SettingsStore.store;
/**
* Settings hook for React components. Returns a smart settings
@@ -223,43 +192,21 @@ export const Settings = makeProxy(settings);
export function useSettings(paths?: UseSettings[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {});
- const onUpdate: SubscriptionCallback = paths
- ? (value, path) => paths.includes(path as UseSettings) && forceUpdate()
- : forceUpdate;
-
React.useEffect(() => {
- subscriptions.add(onUpdate);
- return () => void subscriptions.delete(onUpdate);
+ if (paths) {
+ paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate));
+ return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate));
+ } else {
+ SettingsStore.addGlobalChangeListener(forceUpdate);
+ return () => SettingsStore.removeGlobalChangeListener(forceUpdate);
+ }
}, []);
- return Settings;
-}
-
-// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
-type ResolvePropDeep = P extends "" ? T :
- P extends `${infer Pre}.${infer Suf}` ?
- Pre extends keyof T ? ResolvePropDeep : never : P extends keyof T ? T[P] : never;
-
-/**
- * Add a settings listener that will be invoked whenever the desired setting is updated
- * @param path Path to the setting that you want to watch, for example "plugins.Unindent.enabled" will fire your callback
- * whenever Unindent is toggled. Pass an empty string to get notified for all changes
- * @param onUpdate Callback function whenever a setting matching path is updated. It gets passed the new value and the path
- * to the updated setting. This path will be the same as your path argument, unless it was an empty string.
- *
- * @example addSettingsListener("", (newValue, path) => console.log(`${path} is now ${newValue}`))
- * addSettingsListener("plugins.Unindent.enabled", v => console.log("Unindent is now", v ? "enabled" : "disabled"))
- */
-export function addSettingsListener(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void;
-export function addSettingsListener(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep, path: Path extends "" ? string : Path) => void): void;
-export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) {
- if (path)
- ((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
- subscriptions.add(onUpdate);
+ return SettingsStore.store;
}
export function migratePluginSettings(name: string, ...oldNames: string[]) {
- const { plugins } = settings;
+ const { plugins } = SettingsStore.plain;
if (name in plugins) return;
for (const oldName of oldNames) {
@@ -267,7 +214,7 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
plugins[name] = plugins[oldName];
delete plugins[oldName];
- VencordNative.settings.set(JSON.stringify(settings, null, 4));
+ SettingsStore.markAsChanged();
break;
}
}
diff --git a/src/api/UserSettings.ts b/src/api/UserSettings.ts
new file mode 100644
index 000000000..4de92a81a
--- /dev/null
+++ b/src/api/UserSettings.ts
@@ -0,0 +1,81 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { proxyLazy } from "@utils/lazy";
+import { Logger } from "@utils/Logger";
+import { findModuleId, proxyLazyWebpack, wreq } from "@webpack";
+
+interface UserSettingDefinition {
+ /**
+ * Get the setting value
+ */
+ getSetting(): T;
+ /**
+ * Update the setting value
+ * @param value The new value
+ */
+ updateSetting(value: T): Promise;
+ /**
+ * Update the setting value
+ * @param value A callback that accepts the old value as the first argument, and returns the new value
+ */
+ updateSetting(value: (old: T) => T): Promise;
+ /**
+ * Stateful React hook for this setting value
+ */
+ useSetting(): T;
+ userSettingsAPIGroup: string;
+ userSettingsAPIName: string;
+}
+
+export const UserSettings: Record> | undefined = proxyLazyWebpack(() => {
+ const modId = findModuleId('"textAndImages","renderSpoilers"');
+ if (modId == null) return new Logger("UserSettingsAPI ").error("Didn't find settings module.");
+
+ return wreq(modId as any);
+});
+
+/**
+ * Get the setting with the given setting group and name.
+ *
+ * @param group The setting group
+ * @param name The name of the setting
+ */
+export function getUserSetting(group: string, name: string): UserSettingDefinition | undefined {
+ if (!Vencord.Plugins.isPluginEnabled("UserSettingsAPI")) throw new Error("Cannot use UserSettingsAPI without setting as dependency.");
+
+ for (const key in UserSettings) {
+ const userSetting = UserSettings[key];
+
+ if (userSetting.userSettingsAPIGroup === group && userSetting.userSettingsAPIName === name) {
+ return userSetting;
+ }
+ }
+}
+
+/**
+ * {@link getUserSettingDefinition}, lazy.
+ *
+ * Get the setting with the given setting group and name.
+ *
+ * @param group The setting group
+ * @param name The name of the setting
+ */
+export function getUserSettingLazy(group: string, name: string) {
+ return proxyLazy(() => getUserSetting(group, name));
+}
diff --git a/src/api/index.ts b/src/api/index.ts
index 5dca63105..d4d7b4614 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -26,11 +26,13 @@ import * as $MessageAccessories from "./MessageAccessories";
import * as $MessageDecorations from "./MessageDecorations";
import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover";
+import * as $MessageUpdater from "./MessageUpdater";
import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList";
import * as $Settings from "./Settings";
import * as $Styles from "./Styles";
+import * as $UserSettings from "./UserSettings";
/**
* An API allowing you to listen to Message Clicks or run your own logic
@@ -110,3 +112,13 @@ export const ContextMenu = $ContextMenu;
* An API allowing you to add buttons to the chat input
*/
export const ChatButtons = $ChatButtons;
+
+/**
+ * An API allowing you to update and re-render messages
+ */
+export const MessageUpdater = $MessageUpdater;
+
+/**
+ * An API allowing you to get an user setting
+ */
+export const UserSettings = $UserSettings;
diff --git a/src/components/ExpandableHeader.tsx b/src/components/ExpandableHeader.tsx
index 1cbce4f2e..84b065862 100644
--- a/src/components/ExpandableHeader.tsx
+++ b/src/components/ExpandableHeader.tsx
@@ -16,10 +16,12 @@
* along with this program. If not, see .
*/
+import "./ExpandableHeader.css";
+
import { classNameFactory } from "@api/Styles";
import { Text, Tooltip, useState } from "@webpack/common";
-export const cl = classNameFactory("vc-expandableheader-");
-import "./ExpandableHeader.css";
+
+const cl = classNameFactory("vc-expandableheader-");
export interface ExpandableHeaderProps {
onMoreClick?: () => void;
@@ -31,7 +33,7 @@ export interface ExpandableHeaderProps {
buttons?: React.ReactNode[];
}
-export default function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
+export function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
const [showContent, setShowContent] = useState(defaultState);
return (
diff --git a/src/components/PluginSettings/ContributorModal.tsx b/src/components/PluginSettings/ContributorModal.tsx
index 82c230259..c3c36f1e6 100644
--- a/src/components/PluginSettings/ContributorModal.tsx
+++ b/src/components/PluginSettings/ContributorModal.tsx
@@ -9,20 +9,18 @@ import "./contributorModal.css";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
+import { Link } from "@components/Link";
import { DevsById } from "@utils/constants";
-import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
+import { fetchUserProfile } from "@utils/discord";
+import { classes, pluralise } from "@utils/misc";
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
-import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
+import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import { User } from "discord-types/general";
import Plugins from "~plugins";
import { PluginCard } from ".";
-
-const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
-const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
-const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
-const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
+import { GithubButton, WebsiteButton } from "./LinkIconButton";
const cl = classNameFactory("vc-author-modal-");
@@ -38,16 +36,6 @@ export function openContributorModal(user: User) {
);
}
-function GithubIcon() {
- const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
- return ;
-}
-
-function WebsiteIcon() {
- const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
- return ;
-}
-
function ContributorModal({ user }: { user: User; }) {
useSettings();
@@ -72,6 +60,8 @@ function ContributorModal({ user }: { user: User; }) {
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
}, [user.id, user.username]);
+ const ContributedHyperLink = contributed;
+
return (
<>
@@ -82,32 +72,44 @@ function ContributorModal({ user }: { user: User; }) {
/>
{user.username}
-
+ {plugins.length ? (
+
+ This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
+
+ ) : (
+
+ This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
+
+ )}
+
+ {!!plugins.length && (
+
require("../../plugins"));
const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189");
@@ -68,7 +69,7 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
Restart now to apply new plugins and their settings
-