From 6a7657de3fb49ca85b5da1af45a290f25c5ab464 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 12 Mar 2024 20:18:44 -0300 Subject: [PATCH 01/22] Remove getGuildRoles --- src/plugins/betterRoleContext/index.tsx | 6 +++--- .../components/RolesAndUsersPermissions.tsx | 8 ++++---- src/plugins/permissionsViewer/index.tsx | 3 +-- src/plugins/permissionsViewer/utils.ts | 7 +++---- src/plugins/roleColorEverywhere/index.tsx | 5 ++--- src/plugins/serverProfile/GuildProfileModal.tsx | 6 +++--- src/plugins/xsOverlay.desktop/index.ts | 3 +-- src/utils/discord.tsx | 10 +--------- src/webpack/common/stores.ts | 2 +- src/webpack/common/types/stores.d.ts | 12 +++++++++++- 10 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/plugins/betterRoleContext/index.tsx b/src/plugins/betterRoleContext/index.tsx index e73779ad..3db3494f 100644 --- a/src/plugins/betterRoleContext/index.tsx +++ b/src/plugins/betterRoleContext/index.tsx @@ -5,10 +5,10 @@ */ import { Devs } from "@utils/constants"; -import { getCurrentGuild, getGuildRoles } from "@utils/discord"; +import { getCurrentGuild } from "@utils/discord"; import definePlugin from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { Clipboard, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common"; +import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common"; const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild"); @@ -49,7 +49,7 @@ export default definePlugin({ const guild = getCurrentGuild(); if (!guild) return; - const role = getGuildRoles(guild.id)[id]; + const role = GuildStore.getRole(guild.id, id); if (!role) return; if (role.colorString) { diff --git a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx index adadc90b..c2e50ced 100644 --- a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx +++ b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx @@ -19,9 +19,9 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { InfoIcon, OwnerCrownIcon } from "@components/Icons"; -import { getGuildRoles, getUniqueUsername } from "@utils/discord"; +import { getUniqueUsername } from "@utils/discord"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { ContextMenuApi, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common"; +import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common"; import type { Guild } from "discord-types/general"; import { settings } from ".."; @@ -78,7 +78,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea const [selectedItemIndex, selectItem] = useState(0); const selectedItem = permissions[selectedItemIndex]; - const roles = getGuildRoles(guild.id); + const roles = GuildStore.getRoles(guild.id); return ( { - const role = getGuildRoles(guild.id)[roleId]; + const role = GuildStore.getRole(guild.id, roleId); if (!role) return; onClose(); diff --git a/src/plugins/permissionsViewer/index.tsx b/src/plugins/permissionsViewer/index.tsx index 79ff2a27..b27a3c2f 100644 --- a/src/plugins/permissionsViewer/index.tsx +++ b/src/plugins/permissionsViewer/index.tsx @@ -21,7 +21,6 @@ import "./styles.css"; import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import { getGuildRoles } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; import { ChannelStore, GuildMemberStore, GuildStore, Menu, PermissionsBits, UserStore } from "@webpack/common"; import type { Guild, GuildMember } from "discord-types/general"; @@ -108,7 +107,7 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) { } default: { - permissions = Object.values(getGuildRoles(guild.id)).map(role => ({ + permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({ type: PermissionType.Role, ...role })); diff --git a/src/plugins/permissionsViewer/utils.ts b/src/plugins/permissionsViewer/utils.ts index 11dc7e64..ac753797 100644 --- a/src/plugins/permissionsViewer/utils.ts +++ b/src/plugins/permissionsViewer/utils.ts @@ -17,9 +17,8 @@ */ import { classNameFactory } from "@api/Styles"; -import { getGuildRoles } from "@utils/discord"; import { wordsToTitle } from "@utils/text"; -import { i18n, Parser } from "@webpack/common"; +import { GuildStore, i18n, Parser } from "@webpack/common"; import { Guild, GuildMember, Role } from "discord-types/general"; import type { ReactNode } from "react"; @@ -69,7 +68,7 @@ export function getPermissionDescription(permission: string): ReactNode { } export function getSortedRoles({ id }: Guild, member: GuildMember) { - const roles = getGuildRoles(id); + const roles = GuildStore.getRoles(id); return [...member.roles, id] .map(id => roles[id]) @@ -88,7 +87,7 @@ export function sortUserRoles(roles: Role[]) { } export function sortPermissionOverwrites(overwrites: T[], guildId: string) { - const roles = getGuildRoles(guildId); + const roles = GuildStore.getRoles(guildId); return overwrites.sort((a, b) => { if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0; diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx index eea33fbd..b421eb7f 100644 --- a/src/plugins/roleColorEverywhere/index.tsx +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -19,9 +19,8 @@ import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import { getGuildRoles } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; -import { ChannelStore, GuildMemberStore } from "@webpack/common"; +import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common"; const settings = definePluginSettings({ chatMentions: { @@ -115,7 +114,7 @@ export default definePlugin({ }, roleGroupColor: ErrorBoundary.wrap(({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) => { - const role = getGuildRoles(guildId)[id]; + const role = GuildStore.getRole(guildId, id); return ( 0) { for (const roleId of message.mention_roles) { - const role = getGuildRoles(channel.guild_id)[roleId]; + const role = GuildStore.getRole(channel.guild_id, roleId); if (!role) continue; const roleColor = role.colorString ?? `#${pingColor}`; finalMsg = finalMsg.replace(`<@&${roleId}>`, `@${role.name}`); diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx index 08679db4..74e1aefe 100644 --- a/src/utils/discord.tsx +++ b/src/utils/discord.tsx @@ -18,7 +18,7 @@ import { MessageObject } from "@api/MessageEvents"; import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common"; -import { Guild, Message, Role, User } from "discord-types/general"; +import { Guild, Message, User } from "discord-types/general"; import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal"; @@ -185,11 +185,3 @@ export async function fetchUserProfile(id: string, options?: FetchUserProfileOpt export function getUniqueUsername(user: User) { return user.discriminator === "0" ? user.username : user.tag; } - -// FIXME: remove this once discord merges the role change into stable -export function getGuildRoles(guildId: string): Record { - if ("getRoles" in GuildStore) - return (GuildStore as any).getRoles(guildId); - - return GuildStore.getGuild(guildId)?.roles ?? {}; -} diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index 0c470d6a..f3a18d7b 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -46,7 +46,7 @@ export let ReadStateStore: GenericStore; export let PresenceStore: GenericStore; export let PoggerModeSettingsStore: GenericStore; -export let GuildStore: Stores.GuildStore & t.FluxStore; +export let GuildStore: t.GuildStore; export let UserStore: Stores.UserStore & t.FluxStore; export let UserProfileStore: GenericStore; export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore; diff --git a/src/webpack/common/types/stores.d.ts b/src/webpack/common/types/stores.d.ts index ecc87d74..8e89a6e2 100644 --- a/src/webpack/common/types/stores.d.ts +++ b/src/webpack/common/types/stores.d.ts @@ -17,7 +17,7 @@ */ import { DraftType } from "@webpack/common"; -import { Channel } from "discord-types/general"; +import { Channel, Guild, Role } from "discord-types/general"; import { FluxDispatcher, FluxEvents } from "./utils"; @@ -172,3 +172,13 @@ export class DraftStore extends FluxStore { getThreadDraftWithParentMessageId?(arg: any): any; getThreadSettings(channelId: string): any | null; } + +export class GuildStore extends FluxStore { + getGuild(guildId: string): Guild; + getGuildCount(): number; + getGuilds(): Record; + getGuildIds(): string[]; + getRole(guildId: string, roleId: string): Role; + getRoles(guildId: string): Record; + getAllGuildRoles(): Record>; +} From bf9a2250387b89f131d2e198a265b4e1cb33fea7 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 12 Mar 2024 20:18:46 -0300 Subject: [PATCH 02/22] HTTP Updater: Only include first commit line --- src/main/updater/http.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/updater/http.ts b/src/main/updater/http.ts index 5653d014..605e8d7c 100644 --- a/src/main/updater/http.ts +++ b/src/main/updater/http.ts @@ -53,7 +53,7 @@ async function calculateGitChanges() { // github api only sends the long sha hash: c.sha.slice(0, 7), author: c.author.login, - message: c.commit.message + message: c.commit.message.substring(c.commit.message.indexOf("\n") + 1) })); } From 7190437e92ec006ca865344d0a9c97e9ce065550 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Wed, 13 Mar 2024 00:36:42 +0100 Subject: [PATCH 03/22] rename Devs.obscurity => Devs.fawn :3 --- src/plugins/anonymiseFileNames/index.tsx | 2 +- src/plugins/betterUploadButton/index.ts | 2 +- src/plugins/fakeNitro/index.tsx | 2 +- src/plugins/quickReply/index.ts | 2 +- src/plugins/timeBarAllActivities/index.ts | 2 +- src/plugins/typingIndicator/index.tsx | 2 +- src/utils/constants.ts | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx index 0382b65c..91144023 100644 --- a/src/plugins/anonymiseFileNames/index.tsx +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -67,7 +67,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "AnonymiseFileNames", - authors: [Devs.obscurity], + authors: [Devs.fawn], description: "Anonymise uploaded file names", patches: [ { diff --git a/src/plugins/betterUploadButton/index.ts b/src/plugins/betterUploadButton/index.ts index 3eeb1e45..511d12a4 100644 --- a/src/plugins/betterUploadButton/index.ts +++ b/src/plugins/betterUploadButton/index.ts @@ -21,7 +21,7 @@ import definePlugin from "@utils/types"; export default definePlugin({ name: "BetterUploadButton", - authors: [Devs.obscurity, Devs.Ven], + authors: [Devs.fawn, Devs.Ven], description: "Upload with a single click, open menu with right click", patches: [ { diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 68560817..30744276 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -185,7 +185,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi export default definePlugin({ name: "FakeNitro", - authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz, Devs.AutumnVN], + authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN], description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.", dependencies: ["MessageEventsAPI"], diff --git a/src/plugins/quickReply/index.ts b/src/plugins/quickReply/index.ts index 118a51be..620e1a33 100644 --- a/src/plugins/quickReply/index.ts +++ b/src/plugins/quickReply/index.ts @@ -54,7 +54,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "QuickReply", - authors: [Devs.obscurity, Devs.Ven, Devs.pylix], + authors: [Devs.fawn, Devs.Ven, Devs.pylix], description: "Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds", settings, diff --git a/src/plugins/timeBarAllActivities/index.ts b/src/plugins/timeBarAllActivities/index.ts index ff8fd8b1..dcb809fd 100644 --- a/src/plugins/timeBarAllActivities/index.ts +++ b/src/plugins/timeBarAllActivities/index.ts @@ -22,7 +22,7 @@ import definePlugin from "@utils/types"; export default definePlugin({ name: "TimeBarAllActivities", description: "Adds the Spotify time bar to all activities if they have start and end timestamps", - authors: [Devs.obscurity], + authors: [Devs.fawn], patches: [ { find: "}renderTimeBar(", diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx index 28030148..8bae2f53 100644 --- a/src/plugins/typingIndicator/index.tsx +++ b/src/plugins/typingIndicator/index.tsx @@ -125,7 +125,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "TypingIndicator", description: "Adds an indicator if someone is typing on a channel.", - authors: [Devs.Nuckyz, Devs.obscurity], + authors: [Devs.Nuckyz, Devs.fawn], settings, patches: [ diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 081eed34..40965d08 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -66,8 +66,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "botato", id: 440990343899643943n }, - obscurity: { - name: "obscurity", + fawn: { + name: "fawn", id: 336678828233588736n, }, rushii: { From 9aa205b5ec6dc1f380fd61941a0cb9dfa74f814c Mon Sep 17 00:00:00 2001 From: V Date: Wed, 13 Mar 2024 21:45:45 +0100 Subject: [PATCH 04/22] rewrite settings api to use SettingsStore class (#2257) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> --- browser/VencordNativeStub.ts | 12 +- src/VencordNative.ts | 9 +- src/api/Settings.ts | 159 +++++---------- src/components/VencordSettings/ThemesTab.tsx | 2 +- src/components/VencordSettings/VencordTab.tsx | 15 +- src/main/index.ts | 5 +- src/main/ipcMain.ts | 36 +--- src/main/patcher.ts | 12 +- src/main/settings.ts | 53 +++++ src/main/utils/constants.ts | 1 + .../fixSpotifyEmbeds.desktop/native.ts | 4 +- .../fixYoutubeEmbeds.desktop/native.ts | 4 +- src/shared/SettingsStore.ts | 182 ++++++++++++++++++ src/utils/quickCss.ts | 8 +- src/utils/settingsSync.ts | 8 +- tsconfig.json | 6 +- 16 files changed, 336 insertions(+), 180 deletions(-) create mode 100644 src/main/settings.ts create mode 100644 src/shared/SettingsStore.ts diff --git a/browser/VencordNativeStub.ts b/browser/VencordNativeStub.ts index 70fc1cf9..77c72369 100644 --- a/browser/VencordNativeStub.ts +++ b/browser/VencordNativeStub.ts @@ -26,6 +26,7 @@ import { debounce } from "../src/utils"; import { EXTENSION_BASE_URL } from "../src/utils/web-metadata"; import { getTheme, Theme } from "../src/utils/discord"; import { getThemeInfo } from "../src/main/themes"; +import { Settings } from "../src/Vencord"; // Discord deletes this so need to store in variable const { localStorage } = window; @@ -96,8 +97,15 @@ window.VencordNative = { }, settings: { - get: () => localStorage.getItem("VencordSettings") || "{}", - set: async (s: string) => localStorage.setItem("VencordSettings", s), + get: () => { + try { + return JSON.parse(localStorage.getItem("VencordSettings") || "{}"); + } catch (e) { + console.error("Failed to parse settings from localStorage: ", e); + return {}; + } + }, + set: async (s: Settings) => localStorage.setItem("VencordSettings", JSON.stringify(s)), getSettingsDir: async () => "LocalStorage" }, diff --git a/src/VencordNative.ts b/src/VencordNative.ts index 0faa5569..10381c90 100644 --- a/src/VencordNative.ts +++ b/src/VencordNative.ts @@ -4,11 +4,12 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { PluginIpcMappings } from "@main/ipcPlugins"; +import type { UserThemeHeader } from "@main/themes"; import { IpcEvents } from "@utils/IpcEvents"; import { IpcRes } from "@utils/types"; +import type { Settings } from "api/Settings"; import { ipcRenderer } from "electron"; -import { PluginIpcMappings } from "main/ipcPlugins"; -import type { UserThemeHeader } from "main/themes"; function invoke(event: IpcEvents, ...args: any[]) { return ipcRenderer.invoke(event, ...args) as Promise; @@ -46,8 +47,8 @@ export default { }, settings: { - get: () => sendSync(IpcEvents.GET_SETTINGS), - set: (settings: string) => invoke(IpcEvents.SET_SETTINGS, settings), + get: () => sendSync(IpcEvents.GET_SETTINGS), + set: (settings: Settings, pathToNotify?: string) => invoke(IpcEvents.SET_SETTINGS, settings, pathToNotify), getSettingsDir: () => invoke(IpcEvents.GET_SETTINGS_DIR), }, diff --git a/src/api/Settings.ts b/src/api/Settings.ts index c1ff6915..bd4a4e92 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore"; import { debounce } from "@utils/debounce"; import { localStorage } from "@utils/localStorage"; import { Logger } from "@utils/Logger"; @@ -52,7 +53,6 @@ export interface Settings { | "under-page" | "window" | undefined; - macosTranslucency: boolean | undefined; disableMinSize: boolean; winNativeTitleBar: boolean; plugins: { @@ -88,8 +88,6 @@ const DefaultSettings: Settings = { frameless: false, transparent: false, winCtrlQ: false, - // Replaced by macosVibrancyStyle - macosTranslucency: undefined, macosVibrancyStyle: undefined, disableMinSize: false, winNativeTitleBar: false, @@ -110,13 +108,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 = VencordNative.settings.get(); +mergeDefaults(settings, DefaultSettings); const saveSettingsOnFrequentAction = debounce(async () => { if (Settings.cloud.settingsSync && Settings.cloud.authenticated) { @@ -125,76 +118,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: 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; + } +}); + +SettingsStore.addGlobalChangeListener((_, path) => { + SettingsStore.plain.cloud.settingsSyncVersion = Date.now(); + localStorage.Vencord_settingsDirty = true; + saveSettingsOnFrequentAction(); + VencordNative.settings.set(SettingsStore.plain, path); +}); /** * Same as {@link Settings} but unproxied. You should treat this as readonly, @@ -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,45 +192,21 @@ export const Settings = makeProxy(settings); export function useSettings(paths?: UseSettings[]) { const [, forceUpdate] = React.useReducer(() => ({}), {}); - if (paths) { - (forceUpdate as SubscriptionCallback)._paths = paths; - } - React.useEffect(() => { - subscriptions.add(forceUpdate); - return () => void subscriptions.delete(forceUpdate); + 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) { @@ -269,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/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index 8abaaba4..2eb91cb8 100644 --- a/src/components/VencordSettings/ThemesTab.tsx +++ b/src/components/VencordSettings/ThemesTab.tsx @@ -22,6 +22,7 @@ import { Flex } from "@components/Flex"; import { DeleteIcon } from "@components/Icons"; import { Link } from "@components/Link"; import PluginModal from "@components/PluginSettings/PluginModal"; +import type { UserThemeHeader } from "@main/themes"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; @@ -30,7 +31,6 @@ import { showItemInFolder } from "@utils/native"; import { useAwaiter } from "@utils/react"; import { findByPropsLazy, findLazy } from "@webpack"; import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; -import { UserThemeHeader } from "main/themes"; import type { ComponentType, Ref, SyntheticEvent } from "react"; import { AddonCard } from "./AddonCard"; diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index ab910ea2..c0a66fdc 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -50,14 +50,6 @@ function VencordSettings() { const isMac = navigator.platform.toLowerCase().startsWith("mac"); const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac; - // One-time migration of the old setting to the new one if necessary. - React.useEffect(() => { - if (settings.macosTranslucency === true && !settings.macosVibrancyStyle) { - settings.macosVibrancyStyle = "sidebar"; - settings.macosTranslucency = undefined; - } - }, []); - const Switches: Array; title: string; @@ -164,7 +156,7 @@ function VencordSettings() { options={[ // Sorted from most opaque to most transparent { - label: "No vibrancy", default: !settings.macosTranslucency, value: undefined + label: "No vibrancy", value: undefined }, { label: "Under Page (window tinting)", @@ -191,9 +183,8 @@ function VencordSettings() { value: "header" }, { - label: "Sidebar (old value for transparent windows)", - value: "sidebar", - default: settings.macosTranslucency + label: "Sidebar", + value: "sidebar" }, { label: "Tooltip", diff --git a/src/main/index.ts b/src/main/index.ts index 481736a9..5519d47a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -19,7 +19,8 @@ import { app, protocol, session } from "electron"; import { join } from "path"; -import { ensureSafePath, getSettings } from "./ipcMain"; +import { ensureSafePath } from "./ipcMain"; +import { RendererSettings } from "./settings"; import { IS_VANILLA, THEMES_DIR } from "./utils/constants"; import { installExt } from "./utils/extensions"; @@ -55,7 +56,7 @@ if (IS_VESKTOP || !IS_VANILLA) { }); try { - if (getSettings().enableReactDevtools) + if (RendererSettings.store.enableReactDevtools) installExt("fmkadmapgofadopljbjfkapdkoienihi") .then(() => console.info("[Vencord] Installed React Developer Tools")) .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err)); diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 3ac8a14c..609a581a 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -18,22 +18,21 @@ import "./updater"; import "./ipcPlugins"; +import "./settings"; import { debounce } from "@utils/debounce"; import { IpcEvents } from "@utils/IpcEvents"; -import { Queue } from "@utils/Queue"; import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron"; -import { FSWatcher, mkdirSync, readFileSync, watch } from "fs"; -import { open, readdir, readFile, writeFile } from "fs/promises"; +import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs"; +import { open, readdir, readFile } from "fs/promises"; import { join, normalize } from "path"; import monacoHtml from "~fileContent/monacoWin.html;base64"; import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes"; -import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants"; +import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants"; import { makeLinksOpenExternally } from "./utils/externalLinks"; -mkdirSync(SETTINGS_DIR, { recursive: true }); mkdirSync(THEMES_DIR, { recursive: true }); export function ensureSafePath(basePath: string, path: string) { @@ -71,22 +70,6 @@ function getThemeData(fileName: string) { return readFile(safePath, "utf-8"); } -export function readSettings() { - try { - return readFileSync(SETTINGS_FILE, "utf-8"); - } catch { - return "{}"; - } -} - -export function getSettings(): typeof import("@api/Settings").Settings { - try { - return JSON.parse(readSettings()); - } catch { - return {} as any; - } -} - ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH)); ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { @@ -101,12 +84,10 @@ ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { shell.openExternal(url); }); -const cssWriteQueue = new Queue(); -const settingsWriteQueue = new Queue(); ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss()); ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) => - cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css)) + writeFileSync(QUICKCSS_PATH, css) ); ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR); @@ -117,13 +98,6 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({ "os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}` })); -ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR); -ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings()); - -ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => { - settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s)); -}); - export function initIpc(mainWindow: BrowserWindow) { let quickCssWatcher: FSWatcher | undefined; diff --git a/src/main/patcher.ts b/src/main/patcher.ts index 3ee44d92..ff63ec82 100644 --- a/src/main/patcher.ts +++ b/src/main/patcher.ts @@ -20,7 +20,8 @@ import { onceDefined } from "@utils/onceDefined"; import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron"; import { dirname, join } from "path"; -import { getSettings, initIpc } from "./ipcMain"; +import { initIpc } from "./ipcMain"; +import { RendererSettings } from "./settings"; import { IS_VANILLA } from "./utils/constants"; console.log("[Vencord] Starting up..."); @@ -41,8 +42,7 @@ require.main!.filename = join(asarPath, discordPkg.main); app.setAppPath(asarPath); if (!IS_VANILLA) { - const settings = getSettings(); - + const settings = RendererSettings.store; // Repatch after host updates on Windows if (process.platform === "win32") { require("./patchWin32Updater"); @@ -84,13 +84,11 @@ if (!IS_VANILLA) { options.backgroundColor = "#00000000"; } - const needsVibrancy = process.platform === "darwin" || (settings.macosVibrancyStyle || settings.macosTranslucency); + const needsVibrancy = process.platform === "darwin" && settings.macosVibrancyStyle; if (needsVibrancy) { options.backgroundColor = "#00000000"; - if (settings.macosTranslucency) { - options.vibrancy = "sidebar"; - } else if (settings.macosVibrancyStyle) { + if (settings.macosVibrancyStyle) { options.vibrancy = settings.macosVibrancyStyle; } } diff --git a/src/main/settings.ts b/src/main/settings.ts new file mode 100644 index 00000000..6fe2c3be --- /dev/null +++ b/src/main/settings.ts @@ -0,0 +1,53 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import type { Settings } from "@api/Settings"; +import { SettingsStore } from "@shared/SettingsStore"; +import { IpcEvents } from "@utils/IpcEvents"; +import { ipcMain } from "electron"; +import { mkdirSync, readFileSync, writeFileSync } from "fs"; + +import { NATIVE_SETTINGS_FILE, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants"; + +mkdirSync(SETTINGS_DIR, { recursive: true }); + +function readSettings(name: string, file: string): Partial { + try { + return JSON.parse(readFileSync(file, "utf-8")); + } catch (err: any) { + if (err?.code !== "ENOENT") + console.error(`Failed to read ${name} settings`, err); + + return {}; + } +} + +export const RendererSettings = new SettingsStore(readSettings("renderer", SETTINGS_FILE)); + +RendererSettings.addGlobalChangeListener(() => { + try { + writeFileSync(SETTINGS_FILE, JSON.stringify(RendererSettings.plain, null, 4)); + } catch (e) { + console.error("Failed to write renderer settings", e); + } +}); + +ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR); +ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = RendererSettings.plain); + +ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => { + RendererSettings.setData(data, pathToNotify); +}); + +export const NativeSettings = new SettingsStore(readSettings("native", NATIVE_SETTINGS_FILE)); + +NativeSettings.addGlobalChangeListener(() => { + try { + writeFileSync(NATIVE_SETTINGS_FILE, JSON.stringify(NativeSettings.plain, null, 4)); + } catch (e) { + console.error("Failed to write native settings", e); + } +}); diff --git a/src/main/utils/constants.ts b/src/main/utils/constants.ts index cd6e509f..6c076c32 100644 --- a/src/main/utils/constants.ts +++ b/src/main/utils/constants.ts @@ -28,6 +28,7 @@ export const SETTINGS_DIR = join(DATA_DIR, "settings"); export const THEMES_DIR = join(DATA_DIR, "themes"); export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css"); export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json"); +export const NATIVE_SETTINGS_FILE = join(SETTINGS_DIR, "native-settings.json"); export const ALLOWED_PROTOCOLS = [ "https:", "http:", diff --git a/src/plugins/fixSpotifyEmbeds.desktop/native.ts b/src/plugins/fixSpotifyEmbeds.desktop/native.ts index f779c400..e15e4a44 100644 --- a/src/plugins/fixSpotifyEmbeds.desktop/native.ts +++ b/src/plugins/fixSpotifyEmbeds.desktop/native.ts @@ -4,14 +4,14 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { RendererSettings } from "@main/settings"; import { app } from "electron"; -import { getSettings } from "main/ipcMain"; app.on("browser-window-created", (_, win) => { win.webContents.on("frame-created", (_, { frame }) => { frame.once("dom-ready", () => { if (frame.url.startsWith("https://open.spotify.com/embed/")) { - const settings = getSettings().plugins?.FixSpotifyEmbeds; + const settings = RendererSettings.store.plugins?.FixSpotifyEmbeds; if (!settings?.enabled) return; frame.executeJavaScript(` diff --git a/src/plugins/fixYoutubeEmbeds.desktop/native.ts b/src/plugins/fixYoutubeEmbeds.desktop/native.ts index d5c2df36..003cba9e 100644 --- a/src/plugins/fixYoutubeEmbeds.desktop/native.ts +++ b/src/plugins/fixYoutubeEmbeds.desktop/native.ts @@ -4,14 +4,14 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { RendererSettings } from "@main/settings"; import { app } from "electron"; -import { getSettings } from "main/ipcMain"; app.on("browser-window-created", (_, win) => { win.webContents.on("frame-created", (_, { frame }) => { frame.once("dom-ready", () => { if (frame.url.startsWith("https://www.youtube.com/")) { - const settings = getSettings().plugins?.FixYoutubeEmbeds; + const settings = RendererSettings.store.plugins?.FixYoutubeEmbeds; if (!settings?.enabled) return; frame.executeJavaScript(` diff --git a/src/shared/SettingsStore.ts b/src/shared/SettingsStore.ts new file mode 100644 index 00000000..4109704b --- /dev/null +++ b/src/shared/SettingsStore.ts @@ -0,0 +1,182 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { LiteralUnion } from "type-fest"; + +// 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}` + ? Pre extends keyof T + ? ResolvePropDeep + : any + : P extends keyof T + ? T[P] + : any; + +interface SettingsStoreOptions { + readOnly?: boolean; + getDefaultValue?: (data: { + target: any; + key: string; + root: any; + path: string; + }) => any; +} + +// merges the SettingsStoreOptions type into the class +export interface SettingsStore extends SettingsStoreOptions { } + +/** + * The SettingsStore allows you to easily create a mutable store that + * has support for global and path-based change listeners. + */ +export class SettingsStore { + private pathListeners = new Map void>>(); + private globalListeners = new Set<(newData: T, path: string) => void>(); + + /** + * The store object. Making changes to this object will trigger the applicable change listeners + */ + public declare store: T; + /** + * The plain data. Changes to this object will not trigger any change listeners + */ + public declare plain: T; + + public constructor(plain: T, options: SettingsStoreOptions = {}) { + this.plain = plain; + this.store = this.makeProxy(plain); + Object.assign(this, options); + } + + private makeProxy(object: any, root: T = object, path: string = "") { + const self = this; + + return new Proxy(object, { + get(target, key: string) { + let v = target[key]; + + if (!(key in target) && self.getDefaultValue) { + v = self.getDefaultValue({ + target, + key, + root, + path + }); + } + + if (typeof v === "object" && v !== null && !Array.isArray(v)) + return self.makeProxy(v, root, `${path}${path && "."}${key}`); + + return v; + }, + set(target, key: string, value) { + if (target[key] === value) return true; + + Reflect.set(target, key, value); + const setPath = `${path}${path && "."}${key}`; + + self.globalListeners.forEach(cb => cb(value, setPath)); + self.pathListeners.get(setPath)?.forEach(cb => cb(value)); + + return true; + } + }); + } + + /** + * Set the data of the store. + * This will update this.store and this.plain (and old references to them will be stale! Avoid storing them in variables) + * + * Additionally, all global listeners (and those for pathToNotify, if specified) will be called with the new data + * @param value New data + * @param pathToNotify Optional path to notify instead of globally. Used to transfer path via ipc + */ + public setData(value: T, pathToNotify?: string) { + if (this.readOnly) throw new Error("SettingsStore is read-only"); + + this.plain = value; + this.store = this.makeProxy(value); + + if (pathToNotify) { + let v = value; + + const path = pathToNotify.split("."); + for (const p of path) { + if (!v) { + console.warn( + `Settings#setData: Path ${pathToNotify} does not exist in new data. Not dispatching update` + ); + return; + } + v = v[p]; + } + + this.pathListeners.get(pathToNotify)?.forEach(cb => cb(v)); + } + + this.markAsChanged(); + } + + /** + * Add a global change listener, that will fire whenever any setting is changed + * + * @param data The new data. This is either the new value set on the path, or the new root object if it was changed + * @param path The path of the setting that was changed. Empty string if the root object was changed + */ + public addGlobalChangeListener(cb: (data: any, path: string) => void) { + this.globalListeners.add(cb); + } + + /** + * Add a scoped change listener that will fire whenever a setting matching the specified path is changed. + * + * For example if path is `"foo.bar"`, the listener will fire on + * ```js + * Setting.store.foo.bar = "hi" + * ``` + * but not on + * ```js + * Setting.store.foo.baz = "hi" + * ``` + * @param path + * @param cb + */ + public addChangeListener

>( + path: P, + cb: (data: ResolvePropDeep) => void + ) { + const listeners = this.pathListeners.get(path as string) ?? new Set(); + listeners.add(cb); + this.pathListeners.set(path as string, listeners); + } + + /** + * Remove a global listener + * @see {@link addGlobalChangeListener} + */ + public removeGlobalChangeListener(cb: (data: any, path: string) => void) { + this.globalListeners.delete(cb); + } + + /** + * Remove a scoped listener + * @see {@link addChangeListener} + */ + public removeChangeListener(path: LiteralUnion, cb: (data: any) => void) { + const listeners = this.pathListeners.get(path as string); + if (!listeners) return; + + listeners.delete(cb); + if (!listeners.size) this.pathListeners.delete(path as string); + } + + /** + * Call all global change listeners + */ + public markAsChanged() { + this.globalListeners.forEach(cb => cb(this.plain, "")); + } +} diff --git a/src/utils/quickCss.ts b/src/utils/quickCss.ts index 81320319..99f06004 100644 --- a/src/utils/quickCss.ts +++ b/src/utils/quickCss.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addSettingsListener, Settings } from "@api/Settings"; +import { Settings, SettingsStore } from "@api/Settings"; let style: HTMLStyleElement; @@ -81,10 +81,10 @@ document.addEventListener("DOMContentLoaded", () => { initThemes(); toggle(Settings.useQuickCss); - addSettingsListener("useQuickCss", toggle); + SettingsStore.addChangeListener("useQuickCss", toggle); - addSettingsListener("themeLinks", initThemes); - addSettingsListener("enabledThemes", initThemes); + SettingsStore.addChangeListener("themeLinks", initThemes); + SettingsStore.addChangeListener("enabledThemes", initThemes); if (!IS_WEB) VencordNative.quickCss.addThemeChangeListener(initThemes); diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index 9a0f260a..843922f2 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -36,14 +36,14 @@ export async function importSettings(data: string) { if ("settings" in parsed && "quickCss" in parsed) { Object.assign(PlainSettings, parsed.settings); - await VencordNative.settings.set(JSON.stringify(parsed.settings, null, 4)); + await VencordNative.settings.set(parsed.settings); await VencordNative.quickCss.set(parsed.quickCss); } else throw new Error("Invalid Settings. Is this even a Vencord Settings file?"); } export async function exportSettings({ minify }: { minify?: boolean; } = {}) { - const settings = JSON.parse(VencordNative.settings.get()); + const settings = VencordNative.settings.get(); const quickCss = await VencordNative.quickCss.get(); return JSON.stringify({ settings, quickCss }, null, minify ? undefined : 4); } @@ -137,7 +137,7 @@ export async function putCloudSettings(manual?: boolean) { const { written } = await res.json(); PlainSettings.cloud.settingsSyncVersion = written; - VencordNative.settings.set(JSON.stringify(PlainSettings, null, 4)); + VencordNative.settings.set(PlainSettings); cloudSettingsLogger.info("Settings uploaded to cloud successfully"); @@ -222,7 +222,7 @@ export async function getCloudSettings(shouldNotify = true, force = false) { // sync with server timestamp instead of local one PlainSettings.cloud.settingsSyncVersion = written; - VencordNative.settings.set(JSON.stringify(PlainSettings, null, 4)); + VencordNative.settings.set(PlainSettings); cloudSettingsLogger.info("Settings loaded from cloud successfully"); if (shouldNotify) diff --git a/tsconfig.json b/tsconfig.json index 4563f3f8..e9c92640 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "esnext.asynciterable", "esnext.symbol" ], - "module": "commonjs", + "module": "esnext", "moduleResolution": "node", "strict": true, "noImplicitAny": false, @@ -20,13 +20,15 @@ "baseUrl": "./src/", "paths": { + "@main/*": ["./main/*"], "@api/*": ["./api/*"], "@components/*": ["./components/*"], "@utils/*": ["./utils/*"], + "@shared/*": ["./shared/*"], "@webpack/types": ["./webpack/common/types"], "@webpack/common": ["./webpack/common"], "@webpack": ["./webpack/webpack"] } }, - "include": ["src/**/*"] + "include": ["src/**/*", "browser/**/*", "scripts/**/*"] } From afdcf0edb9d2dc776a77e13e095e3419098b3f52 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Wed, 13 Mar 2024 21:59:09 +0100 Subject: [PATCH 05/22] refactor shared utils to more obviously separate contexts --- src/VencordNative.ts | 2 +- src/api/Settings.ts | 2 +- src/components/VencordSettings/PatchHelperTab.tsx | 2 +- src/main/ipcMain.ts | 4 ++-- src/main/ipcPlugins.ts | 2 +- src/main/patcher.ts | 2 +- src/main/settings.ts | 2 +- src/main/updater/git.ts | 2 +- src/main/updater/http.ts | 4 ++-- .../decor/lib/stores/UsersDecorationsStore.ts | 2 +- src/plugins/imageZoom/index.tsx | 2 +- src/plugins/pinDms/index.tsx | 2 +- src/plugins/pronoundb/pronoundbUtils.ts | 4 ++-- src/plugins/spotifyControls/PlayerComponent.tsx | 2 +- src/preload.ts | 2 +- src/{utils => shared}/IpcEvents.ts | 0 src/{utils => shared}/debounce.ts | 0 src/{utils => shared}/onceDefined.ts | 0 src/shared/vencordUserAgent.ts | 12 ++++++++++++ src/utils/constants.ts | 13 ------------- src/utils/index.ts | 4 ++-- 21 files changed, 32 insertions(+), 33 deletions(-) rename src/{utils => shared}/IpcEvents.ts (100%) rename src/{utils => shared}/debounce.ts (100%) rename src/{utils => shared}/onceDefined.ts (100%) create mode 100644 src/shared/vencordUserAgent.ts diff --git a/src/VencordNative.ts b/src/VencordNative.ts index 10381c90..42e69745 100644 --- a/src/VencordNative.ts +++ b/src/VencordNative.ts @@ -6,7 +6,7 @@ import { PluginIpcMappings } from "@main/ipcPlugins"; import type { UserThemeHeader } from "@main/themes"; -import { IpcEvents } from "@utils/IpcEvents"; +import { IpcEvents } from "@shared/IpcEvents"; import { IpcRes } from "@utils/types"; import type { Settings } from "api/Settings"; import { ipcRenderer } from "electron"; diff --git a/src/api/Settings.ts b/src/api/Settings.ts index bd4a4e92..0b797530 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -16,8 +16,8 @@ * along with this program. If not, see . */ +import { debounce } from "@shared/debounce"; import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore"; -import { debounce } from "@utils/debounce"; import { localStorage } from "@utils/localStorage"; import { Logger } from "@utils/Logger"; import { mergeDefaults } from "@utils/misc"; diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx index 35f46ef5..7e68ec1e 100644 --- a/src/components/VencordSettings/PatchHelperTab.tsx +++ b/src/components/VencordSettings/PatchHelperTab.tsx @@ -18,7 +18,7 @@ import { CheckedTextInput } from "@components/CheckedTextInput"; import { CodeBlock } from "@components/CodeBlock"; -import { debounce } from "@utils/debounce"; +import { debounce } from "@shared/debounce"; import { Margins } from "@utils/margins"; import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; import { makeCodeblock } from "@utils/text"; diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 609a581a..9c9741db 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -20,8 +20,8 @@ import "./updater"; import "./ipcPlugins"; import "./settings"; -import { debounce } from "@utils/debounce"; -import { IpcEvents } from "@utils/IpcEvents"; +import { debounce } from "@shared/debounce"; +import { IpcEvents } from "@shared/IpcEvents"; import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron"; import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs"; import { open, readdir, readFile } from "fs/promises"; diff --git a/src/main/ipcPlugins.ts b/src/main/ipcPlugins.ts index 5d679fc0..5236dbec 100644 --- a/src/main/ipcPlugins.ts +++ b/src/main/ipcPlugins.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { IpcEvents } from "@utils/IpcEvents"; +import { IpcEvents } from "@shared/IpcEvents"; import { ipcMain } from "electron"; import PluginNatives from "~pluginNatives"; diff --git a/src/main/patcher.ts b/src/main/patcher.ts index ff63ec82..0d79a96f 100644 --- a/src/main/patcher.ts +++ b/src/main/patcher.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { onceDefined } from "@utils/onceDefined"; +import { onceDefined } from "@shared/onceDefined"; import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron"; import { dirname, join } from "path"; diff --git a/src/main/settings.ts b/src/main/settings.ts index 6fe2c3be..96efdd67 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -5,8 +5,8 @@ */ import type { Settings } from "@api/Settings"; +import { IpcEvents } from "@shared/IpcEvents"; import { SettingsStore } from "@shared/SettingsStore"; -import { IpcEvents } from "@utils/IpcEvents"; import { ipcMain } from "electron"; import { mkdirSync, readFileSync, writeFileSync } from "fs"; diff --git a/src/main/updater/git.ts b/src/main/updater/git.ts index 20a5d700..82c38b6b 100644 --- a/src/main/updater/git.ts +++ b/src/main/updater/git.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { IpcEvents } from "@utils/IpcEvents"; +import { IpcEvents } from "@shared/IpcEvents"; import { execFile as cpExecFile } from "child_process"; import { ipcMain } from "electron"; import { join } from "path"; diff --git a/src/main/updater/http.ts b/src/main/updater/http.ts index 605e8d7c..9e5a1cef 100644 --- a/src/main/updater/http.ts +++ b/src/main/updater/http.ts @@ -16,8 +16,8 @@ * along with this program. If not, see . */ -import { VENCORD_USER_AGENT } from "@utils/constants"; -import { IpcEvents } from "@utils/IpcEvents"; +import { IpcEvents } from "@shared/IpcEvents"; +import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent"; import { ipcMain } from "electron"; import { writeFile } from "fs/promises"; import { join } from "path"; diff --git a/src/plugins/decor/lib/stores/UsersDecorationsStore.ts b/src/plugins/decor/lib/stores/UsersDecorationsStore.ts index 7295a3b1..b29945f8 100644 --- a/src/plugins/decor/lib/stores/UsersDecorationsStore.ts +++ b/src/plugins/decor/lib/stores/UsersDecorationsStore.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { debounce } from "@utils/debounce"; +import { debounce } from "@shared/debounce"; import { proxyLazy } from "@utils/lazy"; import { useEffect, useState, zustandCreate } from "@webpack/common"; import { User } from "discord-types/general"; diff --git a/src/plugins/imageZoom/index.tsx b/src/plugins/imageZoom/index.tsx index 048c0ed5..cbaa07d2 100644 --- a/src/plugins/imageZoom/index.tsx +++ b/src/plugins/imageZoom/index.tsx @@ -20,8 +20,8 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import { makeRange } from "@components/PluginSettings/components"; +import { debounce } from "@shared/debounce"; import { Devs } from "@utils/constants"; -import { debounce } from "@utils/debounce"; import definePlugin, { OptionType } from "@utils/types"; import { Menu, React, ReactDOM } from "@webpack/common"; import type { Root } from "react-dom/client"; diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index 943f0f1b..45172328 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -26,7 +26,7 @@ import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedD export default definePlugin({ name: "PinDMs", description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs", - authors: [Devs.Ven, Devs.Strencher], + authors: [Devs.Ven], settings, contextMenus, diff --git a/src/plugins/pronoundb/pronoundbUtils.ts b/src/plugins/pronoundb/pronoundbUtils.ts index eac204b7..6373c56a 100644 --- a/src/plugins/pronoundb/pronoundbUtils.ts +++ b/src/plugins/pronoundb/pronoundbUtils.ts @@ -17,8 +17,8 @@ */ import { Settings } from "@api/Settings"; -import { VENCORD_USER_AGENT } from "@utils/constants"; -import { debounce } from "@utils/debounce"; +import { debounce } from "@shared/debounce"; +import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent"; import { getCurrentChannel } from "@utils/discord"; import { useAwaiter } from "@utils/react"; import { UserProfileStore, UserStore } from "@webpack/common"; diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx index 8b3f04bf..ae28631c 100644 --- a/src/plugins/spotifyControls/PlayerComponent.tsx +++ b/src/plugins/spotifyControls/PlayerComponent.tsx @@ -21,7 +21,7 @@ import "./spotifyStyles.css"; import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons"; -import { debounce } from "@utils/debounce"; +import { debounce } from "@shared/debounce"; import { openImageModal } from "@utils/discord"; import { classes, copyWithToast } from "@utils/misc"; import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; diff --git a/src/preload.ts b/src/preload.ts index 08243000..e79eb02c 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { debounce } from "@utils/debounce"; +import { debounce } from "@shared/debounce"; import { contextBridge, webFrame } from "electron"; import { readFileSync, watch } from "fs"; import { join } from "path"; diff --git a/src/utils/IpcEvents.ts b/src/shared/IpcEvents.ts similarity index 100% rename from src/utils/IpcEvents.ts rename to src/shared/IpcEvents.ts diff --git a/src/utils/debounce.ts b/src/shared/debounce.ts similarity index 100% rename from src/utils/debounce.ts rename to src/shared/debounce.ts diff --git a/src/utils/onceDefined.ts b/src/shared/onceDefined.ts similarity index 100% rename from src/utils/onceDefined.ts rename to src/shared/onceDefined.ts diff --git a/src/shared/vencordUserAgent.ts b/src/shared/vencordUserAgent.ts new file mode 100644 index 00000000..0cb1882b --- /dev/null +++ b/src/shared/vencordUserAgent.ts @@ -0,0 +1,12 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import gitHash from "~git-hash"; +import gitRemote from "~git-remote"; + +export { gitHash, gitRemote }; + +export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 40965d08..f609fa64 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -16,17 +16,8 @@ * along with this program. If not, see . */ -import gitHash from "~git-hash"; -import gitRemote from "~git-remote"; - -export { - gitHash, - gitRemote -}; - export const WEBPACK_CHUNK = "webpackChunkdiscord_app"; export const REACT_GLOBAL = "Vencord.Webpack.Common.React"; -export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`; export const SUPPORT_CHANNEL_ID = "1026515880080842772"; export interface Dev { @@ -291,10 +282,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "RyanCaoDev", id: 952235800110694471n, }, - Strencher: { - name: "Strencher", - id: 415849376598982656n - }, FieryFlames: { name: "Fiery", id: 890228870559698955n diff --git a/src/utils/index.ts b/src/utils/index.ts index 90bf8608..ea4adce4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -16,9 +16,10 @@ * along with this program. If not, see . */ +export * from "../shared/debounce"; +export * from "../shared/onceDefined"; export * from "./ChangeList"; export * from "./constants"; -export * from "./debounce"; export * from "./discord"; export * from "./guards"; export * from "./lazy"; @@ -27,7 +28,6 @@ export * from "./Logger"; export * from "./margins"; export * from "./misc"; export * from "./modal"; -export * from "./onceDefined"; export * from "./onlyOnce"; export * from "./patches"; export * from "./Queue"; From f3ee43fe668ab015e5dc33eb02f956a9c0b71886 Mon Sep 17 00:00:00 2001 From: stupid cat Date: Wed, 13 Mar 2024 17:23:04 -0400 Subject: [PATCH 06/22] favGifSearch: don't error on favourited non-urls (#2260) --- src/plugins/favGifSearch/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/plugins/favGifSearch/index.tsx b/src/plugins/favGifSearch/index.tsx index 592d8f54..d71f5679 100644 --- a/src/plugins/favGifSearch/index.tsx +++ b/src/plugins/favGifSearch/index.tsx @@ -200,7 +200,14 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc export function getTargetString(urlStr: string) { - const url = new URL(urlStr); + let url: URL; + try { + url = new URL(urlStr); + } catch (err) { + // Can't resolve URL, return as-is + return urlStr; + } + switch (settings.store.searchOption) { case "url": return url.href; From 6140b9581496a1102839cbefbd2fa1b41a6f4c12 Mon Sep 17 00:00:00 2001 From: Kyuuhachi <1547062+Kyuuhachi@users.noreply.github.com> Date: Sat, 16 Mar 2024 02:19:26 +0100 Subject: [PATCH 07/22] new plugin: BetterSettings ~ improves Discord's settings (#2222) - makes opening settings much faster - removes the scuffed transition animation - organises the settings cog context menu into categories Co-authored-by: Vendicated --- src/plugins/_core/settings.tsx | 26 +--- src/plugins/betterSettings/README.md | 9 ++ src/plugins/betterSettings/index.tsx | 177 +++++++++++++++++++++++ src/webpack/common/components.ts | 3 +- src/webpack/common/types/components.d.ts | 4 + 5 files changed, 199 insertions(+), 20 deletions(-) create mode 100644 src/plugins/betterSettings/README.md create mode 100644 src/plugins/betterSettings/index.tsx diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 01220eb4..569c3f0a 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -import { findGroupChildrenByChildId } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { React, SettingsRouter } from "@webpack/common"; +import { React } from "@webpack/common"; import gitHash from "~git-hash"; @@ -30,23 +29,6 @@ export default definePlugin({ authors: [Devs.Ven, Devs.Megu], required: true, - contextMenus: { - // The settings shortcuts in the user settings cog context menu - // read the elements from a hardcoded map which for obvious reason - // doesn't contain our sections. This patches the actions of our - // sections to manually use SettingsRouter (which only works on desktop - // but the context menu is usually not available on mobile anyway) - "user-settings-cog"(children) { - const section = findGroupChildrenByChildId("VencordSettings", children); - section?.forEach(c => { - const id = c?.props?.id; - if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) { - c!.props.action = () => SettingsRouter.open(id); - } - }); - } - }, - patches: [{ find: ".versionHash", replacement: [ @@ -75,6 +57,12 @@ export default definePlugin({ }, replace: "...$self.makeSettingsCategories($1),$&" } + }, { + find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL", + replacement: { + match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/, + replace: "$2.default.open($1);return;" + } }], customSections: [] as ((SectionTypes: Record) => any)[], diff --git a/src/plugins/betterSettings/README.md b/src/plugins/betterSettings/README.md new file mode 100644 index 00000000..127c6ce7 --- /dev/null +++ b/src/plugins/betterSettings/README.md @@ -0,0 +1,9 @@ +# BetterSettings + +Improves Discord's Settings via multiple (toggleable) changes: +- makes opening settings much faster +- removes the scuffed transition animation +- organises the settings cog context menu into categories + +![](https://github.com/Vendicated/Vencord/assets/45497981/e8d67a95-3909-4be5-8281-8cf9d2f1c30e) + diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx new file mode 100644 index 00000000..6d3c8798 --- /dev/null +++ b/src/plugins/betterSettings/index.tsx @@ -0,0 +1,177 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common"; +import type { HTMLAttributes, ReactElement } from "react"; + +type SettingsEntry = { section: string, label: string; }; + +const cl = classNameFactory(""); +const Classes = findByPropsLazy("animating", "baseLayer", "bg", "layer", "layers"); + +const settings = definePluginSettings({ + disableFade: { + description: "Disable the crossfade animation", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true + }, + organizeMenu: { + description: "Organizes the settings cog context menu into categories", + type: OptionType.BOOLEAN, + default: true + }, + eagerLoad: { + description: "Removes the loading delay when opening the menu for the first time", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true + } +}); + +interface LayerProps extends HTMLAttributes { + mode: "SHOWN" | "HIDDEN"; + baseLayer?: boolean; +} + +function Layer({ mode, baseLayer = false, ...props }: LayerProps) { + const hidden = mode === "HIDDEN"; + const containerRef = useRef(null); + + useEffect(() => () => { + ComponentDispatch.dispatch("LAYER_POP_START"); + ComponentDispatch.dispatch("LAYER_POP_COMPLETE"); + }, []); + + const node = ( +