diff --git a/browser/GMPolyfill.js b/browser/GMPolyfill.js index f8801551e..387389ce6 100644 --- a/browser/GMPolyfill.js +++ b/browser/GMPolyfill.js @@ -62,7 +62,7 @@ function GM_fetch(url, opt) { resp.arrayBuffer = () => blobTo("arrayBuffer", blob); resp.text = () => blobTo("text", blob); resp.json = async () => JSON.parse(await blobTo("text", blob)); - resp.headers = new Headers(parseHeaders(resp.responseHeaders)); + resp.headers = parseHeaders(resp.responseHeaders); resp.ok = resp.status >= 200 && resp.status < 300; resolve(resp); }; diff --git a/browser/VencordNativeStub.ts b/browser/VencordNativeStub.ts index 70fc1cf9d..77c72369c 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/package.json b/package.json index 076b2999d..9d7b6347a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.7", + "version": "1.7.2", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 0a17e8d7e..33b099ef8 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -428,10 +428,11 @@ function runTime(token: string) { if (searchType === "findComponent") method = "find"; if (searchType === "findExportedComponent") method = "findByProps"; - if (searchType === "waitFor" || searchType === "waitForComponent" || searchType === "waitForStore") { + if (searchType === "waitFor" || searchType === "waitForComponent") { if (typeof args[0] === "string") method = "findByProps"; else method = "find"; } + if (searchType === "waitForStore") method = "findStore"; try { let result: any; diff --git a/src/VencordNative.ts b/src/VencordNative.ts index 0faa5569b..42e697452 100644 --- a/src/VencordNative.ts +++ b/src/VencordNative.ts @@ -4,11 +4,12 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { IpcEvents } from "@utils/IpcEvents"; +import { PluginIpcMappings } from "@main/ipcPlugins"; +import type { UserThemeHeader } from "@main/themes"; +import { IpcEvents } from "@shared/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/ContextMenu.ts b/src/api/ContextMenu.ts index d66d98c4f..fdd4facf4 100644 --- a/src/api/ContextMenu.ts +++ b/src/api/ContextMenu.ts @@ -17,22 +17,20 @@ */ import { Logger } from "@utils/Logger"; +import { Menu, React } from "@webpack/common"; import type { ReactElement } from "react"; -type ContextMenuPatchCallbackReturn = (() => void) | void; /** * @param children The rendered context menu elements * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example - * @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates) */ -export type NavContextMenuPatchCallback = (children: Array, ...args: Array) => ContextMenuPatchCallbackReturn; +export type NavContextMenuPatchCallback = (children: Array, ...args: Array) => void; /** * @param navId The navId of the context menu being patched * @param children The rendered context menu elements * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example - * @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates) */ -export type GlobalContextMenuPatchCallback = (navId: string, children: Array, ...args: Array) => ContextMenuPatchCallbackReturn; +export type GlobalContextMenuPatchCallback = (navId: string, children: Array, ...args: Array) => void; const ContextMenuLogger = new Logger("ContextMenu"); @@ -93,14 +91,19 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba * @param id The id of the child. If an array is specified, all ids will be tried * @param children The context menu children */ -export function findGroupChildrenByChildId(id: string | string[], children: Array, _itemsArray?: Array): Array | null { +export function findGroupChildrenByChildId(id: string | string[], children: Array): Array | null { for (const child of children) { if (child == null) continue; + if (Array.isArray(child)) { + const found = findGroupChildrenByChildId(id, child); + if (found !== null) return found; + } + if ( (Array.isArray(id) && id.some(id => child.props?.id === id)) || child.props?.id === id - ) return _itemsArray ?? null; + ) return children; let nextChildren = child.props?.children; if (nextChildren) { @@ -109,7 +112,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra child.props.children = nextChildren; } - const found = findGroupChildrenByChildId(id, nextChildren, nextChildren); + const found = findGroupChildrenByChildId(id, nextChildren); if (found !== null) return found; } } @@ -126,9 +129,12 @@ interface ContextMenuProps { onClose: (callback: (...args: Array) => any) => void; } -const patchedMenus = new WeakSet(); +export function _usePatchContextMenu(props: ContextMenuProps) { + props = { + ...props, + children: cloneMenuChildren(props.children), + }; -export function _patchContextMenu(props: ContextMenuProps) { props.contextMenuApiArguments ??= []; const contextMenuPatches = navPatches.get(props.navId); @@ -137,8 +143,7 @@ export function _patchContextMenu(props: ContextMenuProps) { if (contextMenuPatches) { for (const patch of contextMenuPatches) { try { - const callback = patch(props.children, ...props.contextMenuApiArguments); - if (!patchedMenus.has(props)) callback?.(); + patch(props.children, ...props.contextMenuApiArguments); } catch (err) { ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); } @@ -147,12 +152,30 @@ export function _patchContextMenu(props: ContextMenuProps) { for (const patch of globalPatches) { try { - const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments); - if (!patchedMenus.has(props)) callback?.(); + patch(props.navId, props.children, ...props.contextMenuApiArguments); } catch (err) { ContextMenuLogger.error("Global patch errored,", err); } } - patchedMenus.add(props); + return props; +} + +function cloneMenuChildren(obj: ReactElement | Array | null) { + if (Array.isArray(obj)) { + return obj.map(cloneMenuChildren); + } + + if (React.isValidElement(obj)) { + obj = React.cloneElement(obj); + + if ( + obj?.props?.children && + (obj.type !== Menu.MenuControlItem || obj.type === Menu.MenuControlItem && obj.props.control != null) + ) { + obj.props.children = cloneMenuChildren(obj.props.children); + } + } + + return obj; } diff --git a/src/api/MessageEvents.ts b/src/api/MessageEvents.ts index 341b4e678..d6eba748f 100644 --- a/src/api/MessageEvents.ts +++ b/src/api/MessageEvents.ts @@ -74,7 +74,7 @@ export interface MessageExtra { } export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable; -export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable; +export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable; const sendListeners = new Set(); const editListeners = new Set(); @@ -84,7 +84,7 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec for (const listener of sendListeners) { try { const result = await listener(channelId, messageObj, extra); - if (result && result.cancel === true) { + if (result?.cancel) { return true; } } catch (e) { @@ -97,11 +97,15 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) { for (const listener of editListeners) { try { - await listener(channelId, messageId, messageObj); + const result = await listener(channelId, messageId, messageObj); + if (result?.cancel) { + return true; + } } catch (e) { MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e); } } + return false; } /** diff --git a/src/api/Notifications/notificationLog.tsx b/src/api/Notifications/notificationLog.tsx index 9535fb62c..6f79ef70a 100644 --- a/src/api/Notifications/notificationLog.tsx +++ b/src/api/Notifications/notificationLog.tsx @@ -21,7 +21,7 @@ import { Settings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; -import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; +import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; import { nanoid } from "nanoid"; import type { DispatchWithoutAction } from "react"; @@ -129,7 +129,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) { richBody={
{data.body} - +
} /> diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 004a8988b..0b7975300 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -16,7 +16,8 @@ * 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"; @@ -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,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/components/VencordSettings/CloudTab.tsx b/src/components/VencordSettings/CloudTab.tsx index 0392a451c..080dd8dd9 100644 --- a/src/components/VencordSettings/CloudTab.tsx +++ b/src/components/VencordSettings/CloudTab.tsx @@ -39,9 +39,7 @@ function validateUrl(url: string) { async function eraseAllData() { const res = await fetch(new URL("/v1/", getCloudUrl()), { method: "DELETE", - headers: new Headers({ - Authorization: await getCloudAuth() - }) + headers: { Authorization: await getCloudAuth() } }); if (!res.ok) { diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx index 35f46ef50..7e68ec1e7 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/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index 8abaaba4f..2eb91cb82 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 ab910ea2a..c0a66fdc7 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 481736a98..5519d47ac 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 47d400eb6..9c9741db5 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 { debounce } from "@shared/debounce"; +import { IpcEvents } from "@shared/IpcEvents"; import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron"; -import { 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,25 +98,25 @@ 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; + open(QUICKCSS_PATH, "a+").then(fd => { fd.close(); - watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { + quickCssWatcher = watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); }, 50)); - }); + }).catch(() => { }); - watch(THEMES_DIR, { persistent: false }, debounce(() => { + const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => { mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0); })); + + mainWindow.once("closed", () => { + quickCssWatcher?.close(); + themesWatcher.close(); + }); } ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { diff --git a/src/main/ipcPlugins.ts b/src/main/ipcPlugins.ts index 5d679fc0b..5236dbec4 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 3ee44d92c..0d79a96f6 100644 --- a/src/main/patcher.ts +++ b/src/main/patcher.ts @@ -16,11 +16,12 @@ * 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"; -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 000000000..96efdd672 --- /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 { IpcEvents } from "@shared/IpcEvents"; +import { SettingsStore } from "@shared/SettingsStore"; +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/updater/git.ts b/src/main/updater/git.ts index 2ff3ba512..82c38b6bc 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"; @@ -49,9 +49,12 @@ async function getRepo() { async function calculateGitChanges() { await git("fetch"); - const branch = await git("branch", "--show-current"); + const branch = (await git("branch", "--show-current")).stdout.trim(); - const res = await git("log", `HEAD...origin/${branch.stdout.trim()}`, "--pretty=format:%an/%h/%s"); + const existsOnOrigin = (await git("ls-remote", "origin", branch)).stdout.length > 0; + if (!existsOnOrigin) return []; + + const res = await git("log", `HEAD...origin/${branch}`, "--pretty=format:%an/%h/%s"); const commits = res.stdout.trim(); return commits ? commits.split("\n").map(line => { diff --git a/src/main/updater/http.ts b/src/main/updater/http.ts index 5653d0143..9e5a1cef4 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"; @@ -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) })); } diff --git a/src/main/utils/constants.ts b/src/main/utils/constants.ts index cd6e509f7..6c076c328 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/_api/contextMenu.ts b/src/plugins/_api/contextMenu.ts index 55fdf3eae..01619546d 100644 --- a/src/plugins/_api/contextMenu.ts +++ b/src/plugins/_api/contextMenu.ts @@ -22,15 +22,15 @@ import definePlugin from "@utils/types"; export default definePlugin({ name: "ContextMenuAPI", description: "API for adding/removing items to/from context menus.", - authors: [Devs.Nuckyz, Devs.Ven], + authors: [Devs.Nuckyz, Devs.Ven, Devs.Kyuuhachi], required: true, patches: [ { find: "♫ (つ。◕‿‿◕。)つ ♪", replacement: { - match: /let{navId:/, - replace: "Vencord.Api.ContextMenu._patchContextMenu(arguments[0]);$&" + match: /(?=let{navId:)(?<=function \i\((\i)\).+?)/, + replace: "$1=Vencord.Api.ContextMenu._usePatchContextMenu($1);" } }, { diff --git a/src/plugins/_api/messageEvents.ts b/src/plugins/_api/messageEvents.ts index bc5f5abf2..1b4a2d15a 100644 --- a/src/plugins/_api/messageEvents.ts +++ b/src/plugins/_api/messageEvents.ts @@ -25,10 +25,13 @@ export default definePlugin({ authors: [Devs.Arjix, Devs.hunt, Devs.Ven], patches: [ { - find: '"MessageActionCreators"', + find: ".Messages.EDIT_TEXTAREA_HELP", replacement: { - match: /async editMessage\(.+?\)\{/, - replace: "$&await Vencord.Api.MessageEvents._handlePreEdit(...arguments);" + match: /(?<=,channel:\i\}\)\.then\().+?(?=return \i\.content!==this\.props\.message\.content&&\i\((.+?)\))/, + replace: (match, args) => "" + + `async ${match}` + + `if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` + + "return Promise.resolve({shoudClear:true,shouldRefocus:true});" } }, { diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 6f43b76a8..569c3f0ac 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 { addContextMenuPatch } 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, - start() { - // 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) - addContextMenuPatch("user-settings-cog", children => () => { - const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any; - 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/alwaysTrust/index.ts b/src/plugins/alwaysTrust/index.ts index 07e92afce..5113935f4 100644 --- a/src/plugins/alwaysTrust/index.ts +++ b/src/plugins/alwaysTrust/index.ts @@ -16,27 +16,46 @@ * along with this program. If not, see . */ +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; + +const settings = definePluginSettings({ + domain: { + type: OptionType.BOOLEAN, + default: true, + description: "Remove the untrusted domain popup when opening links", + restartNeeded: true + }, + file: { + type: OptionType.BOOLEAN, + default: true, + description: "Remove the 'Potentially Dangerous Download' popup when opening links", + restartNeeded: true + } +}); export default definePlugin({ name: "AlwaysTrust", description: "Removes the annoying untrusted domain and suspicious file popup", - authors: [Devs.zt], + authors: [Devs.zt, Devs.Trwy], patches: [ { find: ".displayName=\"MaskedLinkStore\"", replacement: { match: /(?<=isTrustedDomain\(\i\){)return \i\(\i\)/, replace: "return true" - } + }, + predicate: () => settings.store.domain }, { find: "isSuspiciousDownload:", replacement: { match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/, replace: "$&return null;" - } + }, + predicate: () => settings.store.file } - ] + ], + settings }); diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx index 0382b65c2..b424b7a59 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: [ { @@ -78,6 +78,13 @@ export default definePlugin({ "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),", }, }, + { + find: "message.attachments", + replacement: { + match: /(\i.uploadFiles\((\i),)/, + replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1" + } + }, { find: ".Messages.ATTACHMENT_UTILITIES_SPOILER", replacement: { diff --git a/src/plugins/betterRoleContext/README.md b/src/plugins/betterRoleContext/README.md new file mode 100644 index 000000000..3f3086bdb --- /dev/null +++ b/src/plugins/betterRoleContext/README.md @@ -0,0 +1,6 @@ +# BetterRoleContext + +Adds options to copy role color and edit role when right clicking roles in the user profile + +![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326) + diff --git a/src/plugins/betterRoleContext/index.tsx b/src/plugins/betterRoleContext/index.tsx new file mode 100644 index 000000000..3db3494f9 --- /dev/null +++ b/src/plugins/betterRoleContext/index.tsx @@ -0,0 +1,81 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import { getCurrentGuild } from "@utils/discord"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common"; + +const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild"); + +function PencilIcon() { + return ( + + + + ); +} + +function AppearanceIcon() { + return ( + + + + ); +} + +export default definePlugin({ + name: "BetterRoleContext", + description: "Adds options to copy role color / edit role when right clicking roles in the user profile", + authors: [Devs.Ven], + + start() { + // DeveloperMode needs to be enabled for the context menu to be shown + TextAndImagesSettingsStores.DeveloperMode.updateSetting(true); + }, + + contextMenus: { + "dev-context"(children, { id }: { id: string; }) { + const guild = getCurrentGuild(); + if (!guild) return; + + const role = GuildStore.getRole(guild.id, id); + if (!role) return; + + if (role.colorString) { + children.push( + Clipboard.copy(role.colorString!)} + icon={AppearanceIcon} + /> + ); + } + + if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) { + children.push( + { + await GuildSettingsActions.open(guild.id, "ROLES"); + GuildSettingsActions.selectRole(id); + }} + icon={PencilIcon} + /> + ); + } + } + } +}); diff --git a/src/plugins/betterSettings/README.md b/src/plugins/betterSettings/README.md new file mode 100644 index 000000000..127c6ce76 --- /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 000000000..6d3c8798e --- /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 = ( + diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 906bed504..2d091c24a 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -29,7 +29,7 @@ import type { Channel, Role } from "discord-types/general"; import HiddenChannelLockScreen from "./components/HiddenChannelLockScreen"; -const ChannelListClasses = findByPropsLazy("channelEmoji", "unread", "icon"); +const ChannelListClasses = findByPropsLazy("modeMuted", "modeSelected", "unread", "icon"); const enum ShowMode { LockIcon, @@ -162,7 +162,7 @@ export default definePlugin({ }, // Add the hidden eye icon if the channel is hidden { - match: /\i\.children.+?:null(?<=,channel:(\i).+?)/, + match: /\.name\),.{0,120}\.children.+?:null(?<=,channel:(\i).+?)/, replace: (m, channel) => `${m},$self.isHiddenChannel(${channel})?$self.HiddenChannelIcon():null` }, // Make voice channels also appear as muted if they are muted @@ -305,27 +305,27 @@ export default definePlugin({ ] }, { - find: ".avatars),children", + find: '+1]})},"overflow"))', replacement: [ { // Create a variable for the channel prop - match: /maxUsers:\i,users:\i.+?=(\i).+?;/, + match: /maxUsers:\i,users:\i.+?}=(\i).*?;/, replace: (m, props) => `${m}let{shcChannel}=${props};` }, { // Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen match: /\i>0(?=&&.{0,60}renderPopout)/, - replace: m => `($self.isHiddenChannel(shcChannel,true)?true:${m})` + replace: m => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)?true:${m})` }, { // Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/, - replace: (_, amount) => `($self.isHiddenChannel(shcChannel,true)&&${amount}<=0?0:1)` + replace: (_, amount) => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?0:1)` }, { // Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen match: /(?<="\+",)(\i)\+1/, - replace: (m, amount) => `$self.isHiddenChannel(shcChannel,true)&&${amount}<=0?"":${m}` + replace: (m, amount) => `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?"":${m}` } ] }, diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx index f2370906b..ae28631c9 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"; @@ -371,6 +371,10 @@ export function Player() { if (!track || !device?.is_active || shouldHide) return null; + const exportTrackImageStyle = { + "--vc-spotify-track-image": `url(${track?.album?.image?.url || ""})`, + } as React.CSSProperties; + return ( (
@@ -378,7 +382,7 @@ export function Player() {

Check the console for errors

)}> -
+
diff --git a/src/plugins/spotifyControls/index.tsx b/src/plugins/spotifyControls/index.tsx index cfb352efe..d7e4f6454 100644 --- a/src/plugins/spotifyControls/index.tsx +++ b/src/plugins/spotifyControls/index.tsx @@ -31,7 +31,7 @@ function toggleHoverControls(value: boolean) { export default definePlugin({ name: "SpotifyControls", description: "Adds a Spotify player above the account panel", - authors: [Devs.Ven, Devs.afn, Devs.KraXen72], + authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000], options: { hoverControls: { description: "Show controls on hover", diff --git a/src/plugins/spotifyControls/spotifyStyles.css b/src/plugins/spotifyControls/spotifyStyles.css index 9e585ebec..72383c3e8 100644 --- a/src/plugins/spotifyControls/spotifyStyles.css +++ b/src/plugins/spotifyControls/spotifyStyles.css @@ -170,9 +170,16 @@ /* these importants are necessary, it applies a width and height through inline styles */ height: 10px !important; width: 10px !important; + margin-top: 4px; background-color: var(--interactive-normal); border-color: var(--interactive-normal); color: var(--interactive-normal); + opacity: 0; + transition: opacity 0.1s; +} + +#vc-spotify-progress-bar:hover > [class^="slider"] [class^="grabber"] { + opacity: 1; } #vc-spotify-progress-text { diff --git a/src/plugins/superReactionTweaks/index.ts b/src/plugins/superReactionTweaks/index.ts index 0e58eb0a8..89197b4c3 100644 --- a/src/plugins/superReactionTweaks/index.ts +++ b/src/plugins/superReactionTweaks/index.ts @@ -7,6 +7,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; +import { UserStore } from "@webpack/common"; export const settings = definePluginSettings({ superReactByDefault: { @@ -49,7 +50,7 @@ export default definePlugin({ find: ".trackEmojiSearchEmpty,200", replacement: { match: /(\.trackEmojiSearchEmpty,200(?=.+?isBurstReaction:(\i).+?(\i===\i\.EmojiIntention.REACTION)).+?\[\2,\i\]=\i\.useState\().+?\)/, - replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.settings.store.superReactByDefault&&${isReactionIntention})` + replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.shouldSuperReactByDefault&&${isReactionIntention})` } } ], @@ -59,5 +60,9 @@ export default definePlugin({ if (settings.store.unlimitedSuperReactionPlaying) return true; if (playingCount <= settings.store.superReactionPlayingLimit) return true; return false; + }, + + get shouldSuperReactByDefault() { + return settings.store.superReactByDefault && UserStore.getCurrentUser().premiumType != null; } }); diff --git a/src/plugins/timeBarAllActivities/index.ts b/src/plugins/timeBarAllActivities/index.ts index ff8fd8b17..dcb809fd4 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/translate/index.tsx b/src/plugins/translate/index.tsx index 702e60cf7..f602d1255 100644 --- a/src/plugins/translate/index.tsx +++ b/src/plugins/translate/index.tsx @@ -19,7 +19,7 @@ import "./styles.css"; import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons"; -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { addAccessory, removeAccessory } from "@api/MessageAccessories"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; import { addButton, removeButton } from "@api/MessagePopover"; @@ -32,7 +32,7 @@ import { TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon"; import { handleTranslate, TranslationAccessory } from "./TranslationAccessory"; import { translate } from "./utils"; -const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => () => { +const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => { if (!message.content) return; const group = findGroupChildrenByChildId("copy-text", children); @@ -57,13 +57,15 @@ export default definePlugin({ authors: [Devs.Ven], dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"], settings, + contextMenus: { + "message": messageCtxPatch + }, // not used, just here in case some other plugin wants it or w/e translate, start() { addAccessory("vc-translation", props => ); - addContextMenuPatch("message", messageCtxPatch); addChatBarButton("vc-translate", TranslateChatBarIcon); addButton("vc-translate", message => { @@ -91,7 +93,6 @@ export default definePlugin({ stop() { removePreSendListener(this.preSend); - removeContextMenuPatch("message", messageCtxPatch); removeChatBarButton("vc-translate"); removeButton("vc-translate"); removeAccessory("vc-translation"); diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx index 171c560d8..8bae2f53c 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: [ @@ -133,7 +133,7 @@ export default definePlugin({ { find: "UNREAD_IMPORTANT:", replacement: { - match: /channel:(\i).{0,100}?channelEmoji,.{0,250}?\.children.{0,50}?:null/, + match: /\.name\),.{0,120}\.children.+?:null(?<=,channel:(\i).+?)/, replace: "$&,$self.TypingIndicator($1.id)" } }, diff --git a/src/plugins/unsuppressEmbeds/index.tsx b/src/plugins/unsuppressEmbeds/index.tsx index a21960774..0e87201c6 100644 --- a/src/plugins/unsuppressEmbeds/index.tsx +++ b/src/plugins/unsuppressEmbeds/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { ImageInvisible, ImageVisible } from "@components/Icons"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -24,7 +24,7 @@ import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@web const EMBED_SUPPRESSED = 1 << 2; -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => () => { +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => { const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0; if (!isEmbedSuppressed && !embeds.length) return; @@ -56,12 +56,7 @@ export default definePlugin({ name: "UnsuppressEmbeds", authors: [Devs.rad, Devs.HypedDomi], description: "Allows you to unsuppress embeds in messages", - - start() { - addContextMenuPatch("message", messageContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("message", messageContextMenuPatch); - }, + contextMenus: { + "message": messageContextMenuPatch + } }); diff --git a/src/plugins/userVoiceShow/index.tsx b/src/plugins/userVoiceShow/index.tsx index c95307cc4..935ff1c5d 100644 --- a/src/plugins/userVoiceShow/index.tsx +++ b/src/plugins/userVoiceShow/index.tsx @@ -96,7 +96,7 @@ export default definePlugin({ patches: [ // above message box { - find: ".lastEditedByContainer", + find: ".popularApplicationCommandIds,", replacement: { match: /\(0,\i\.jsx\)\(\i\.\i,{user:\i,setNote/, replace: "$self.patchPopout(arguments[0]),$&", diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx index 0a805a0d2..00805fbd3 100644 --- a/src/plugins/vencordToolbox/index.tsx +++ b/src/plugins/vencordToolbox/index.tsx @@ -19,7 +19,7 @@ import "./index.css"; import { openNotificationLogModal } from "@api/Notifications/notificationLog"; -import { Settings } from "@api/Settings"; +import { Settings, useSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -30,6 +30,8 @@ import type { ReactNode } from "react"; const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider"); function VencordPopout(onClose: () => void) { + const { useQuickCss } = useSettings(["useQuickCss"]); + const pluginEntries = [] as ReactNode[]; for (const plugin of Object.values(Vencord.Plugins.plugins)) { @@ -68,11 +70,10 @@ function VencordPopout(onClose: () => void) { /> { - Settings.useQuickCss = !Settings.useQuickCss; - onClose(); + Settings.useQuickCss = !useQuickCss; }} /> . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { ImageIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { openImageModal } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { GuildMemberStore, Menu } from "@webpack/common"; +import { GuildMemberStore, IconUtils, Menu } from "@webpack/common"; import type { Channel, Guild, User } from "discord-types/general"; -const BannerStore = findByPropsLazy("getGuildBannerURL"); interface UserContextProps { channel: Channel; @@ -82,7 +80,7 @@ function openImage(url: string) { }); } -const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => { +const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => { if (!user) return; const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null; @@ -91,19 +89,19 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U openImage(BannerStore.getUserAvatarURL(user, true))} + action={() => openImage(IconUtils.getUserAvatarURL(user, true))} icon={ImageIcon} /> {memberAvatar && ( openImage(BannerStore.getGuildMemberAvatarURLSimple({ + action={() => openImage(IconUtils.getGuildMemberAvatarURLSimple({ userId: user.id, avatar: memberAvatar, - guildId, + guildId: guildId!, canAnimate: true - }, true))} + }))} icon={ImageIcon} /> )} @@ -111,7 +109,7 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U )); }; -const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => () => { +const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => { if (!guild) return; const { id, icon, banner } = guild; @@ -124,11 +122,11 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon id="view-icon" label="View Icon" action={() => - openImage(BannerStore.getGuildIconURL({ + openImage(IconUtils.getGuildIconURL({ id, icon, canAnimate: true - })) + })!) } icon={ImageIcon} /> @@ -138,10 +136,7 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon id="view-banner" label="View Banner" action={() => - openImage(BannerStore.getGuildBannerURL({ - id, - banner, - }, true)) + openImage(IconUtils.getGuildBannerURL(guild, true)!) } icon={ImageIcon} /> @@ -160,14 +155,9 @@ export default definePlugin({ openImage, - start() { - addContextMenuPatch("user-context", UserContext); - addContextMenuPatch("guild-context", GuildContext); - }, - - stop() { - removeContextMenuPatch("user-context", UserContext); - removeContextMenuPatch("guild-context", GuildContext); + contextMenus: { + "user-context": UserContext, + "guild-context": GuildContext }, patches: [ @@ -184,7 +174,7 @@ export default definePlugin({ find: ".NITRO_BANNER,", replacement: { // style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl, - match: /style:\{(?=backgroundImage:(\i&&\i)\?"url\("\.concat\((\i),)/, + match: /style:\{(?=backgroundImage:(\i)\?"url\("\.concat\((\i),)/, replace: // onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0, 'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,' diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index 08acdc4c5..68b33eed0 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { addButton, removeButton } from "@api/MessagePopover"; import { definePluginSettings } from "@api/Settings"; import { CodeBlock } from "@components/CodeBlock"; @@ -117,8 +117,8 @@ const settings = definePluginSettings({ } }); -function MakeContextCallback(name: "Guild" | "User" | "Channel") { - const callback: NavContextMenuPatchCallback = (children, props) => () => { +function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback { + return (children, props) => { const value = props[name.toLowerCase()]; if (!value) return; if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings @@ -141,16 +141,19 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel") { /> ); }; - return callback; } - export default definePlugin({ name: "ViewRaw", description: "Copy and view the raw content/data of any message, channel or guild", authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna], dependencies: ["MessagePopoverAPI"], settings, + contextMenus: { + "guild-context": MakeContextCallback("Guild"), + "channel-context": MakeContextCallback("Channel"), + "user-context": MakeContextCallback("User") + }, start() { addButton("ViewRaw", msg => { @@ -187,16 +190,9 @@ export default definePlugin({ onContextMenu: handleContextMenu }; }); - - addContextMenuPatch("guild-context", MakeContextCallback("Guild")); - addContextMenuPatch("channel-context", MakeContextCallback("Channel")); - addContextMenuPatch("user-context", MakeContextCallback("User")); }, stop() { - removeButton("CopyRawMessage"); - removeContextMenuPatch("guild-context", MakeContextCallback("Guild")); - removeContextMenuPatch("channel-context", MakeContextCallback("Channel")); - removeContextMenuPatch("user-context", MakeContextCallback("User")); + removeButton("ViewRaw"); } }); diff --git a/src/plugins/voiceMessages/index.tsx b/src/plugins/voiceMessages/index.tsx index f4898de68..2f232f341 100644 --- a/src/plugins/voiceMessages/index.tsx +++ b/src/plugins/voiceMessages/index.tsx @@ -18,15 +18,17 @@ import "./styles.css"; -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Microphone } from "@components/Icons"; +import { Link } from "@components/Link"; import { Devs } from "@utils/constants"; +import { Margins } from "@utils/margins"; import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; import definePlugin from "@utils/types"; import { chooseFile } from "@utils/web"; import { findByPropsLazy, findStoreLazy } from "@webpack"; -import { Button, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; +import { Button, Card, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; import { ComponentType } from "react"; import { VoiceRecorderDesktop } from "./DesktopRecorder"; @@ -46,18 +48,30 @@ export type VoiceRecorder = ComponentType<{ const VoiceRecorder = IS_DISCORD_DESKTOP ? VoiceRecorderDesktop : VoiceRecorderWeb; +const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => { + if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return; + + children.push( + + +
Send voice message
+
+ } + action={() => openModal(modalProps => )} + /> + ); +}; + export default definePlugin({ name: "VoiceMessages", description: "Allows you to send voice messages like on mobile. To do so, right click the upload button and click Send Voice Message", authors: [Devs.Ven, Devs.Vap, Devs.Nickyux], settings, - - start() { - addContextMenuPatch("channel-attach", ctxMenuPatch); - }, - - stop() { - removeContextMenuPatch("channel-attach", ctxMenuPatch); + contextMenus: { + "channel-attach": ctxMenuPatch } }); @@ -164,6 +178,11 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) { fallbackValue: EMPTY_META, }); + const isUnsupportedFormat = blob && ( + !blob.type.startsWith("audio/ogg") + || blob.type.includes("codecs") && !blob.type.includes("opus") + ); + return ( @@ -200,6 +219,16 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) { recording={isRecording} /> + {isUnsupportedFormat && ( + + Voice Messages have to be OggOpus to be playable on iOS. This file is {blob.type} so it will not be playable on iOS. + + + To fix it, first convert it to OggOpus, for example using the convertio web converter + + + )} + @@ -217,20 +246,3 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) { ); } - -const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { - if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return; - - children.push( - - -
Send voice message
-
- } - action={() => openModal(modalProps => )} - /> - ); -}; diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index 5f6beca2c..faa240783 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -47,18 +47,23 @@ const settings = definePluginSettings({ }); const MEDIA_PROXY_URL = "https://media.discordapp.net"; -const CDN_URL = "https://cdn.discordapp.com"; +const CDN_URL = "cdn.discordapp.com"; -function fixImageUrl(urlString: string, explodeWebp: boolean) { +function fixImageUrl(urlString: string) { const url = new URL(urlString); - if (url.origin === CDN_URL) return urlString; - if (url.origin === MEDIA_PROXY_URL) return CDN_URL + url.pathname; + if (url.host === CDN_URL) return urlString; url.searchParams.delete("width"); url.searchParams.delete("height"); - url.searchParams.set("quality", "lossless"); - if (explodeWebp && url.searchParams.get("format") === "webp") - url.searchParams.set("format", "png"); + + if (url.origin === MEDIA_PROXY_URL) { + url.host = CDN_URL; + url.searchParams.delete("size"); + url.searchParams.delete("quality"); + url.searchParams.delete("format"); + } else { + url.searchParams.set("quality", "lossless"); + } return url.toString(); } @@ -199,7 +204,7 @@ export default definePlugin({ ], async copyImage(url: string) { - url = fixImageUrl(url, true); + url = fixImageUrl(url); let imageData = await fetch(url).then(r => r.blob()); if (imageData.type !== "image/png") { @@ -231,7 +236,7 @@ export default definePlugin({ }, async saveImage(url: string) { - url = fixImageUrl(url, false); + url = fixImageUrl(url); const data = await fetchImage(url); if (!data) return; diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx index 4a2bdeeda..b60366900 100644 --- a/src/plugins/whoReacted/index.tsx +++ b/src/plugins/whoReacted/index.tsx @@ -29,7 +29,7 @@ import { Message, ReactionEmoji, User } from "discord-types/general"; const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); - +let Scroll: any = null; const queue = new Queue(); let reactions: Record; @@ -69,14 +69,14 @@ function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) { function makeRenderMoreUsers(users: User[]) { return function renderMoreUsers(_label: string, _count: number) { return ( - u.username).join(", ")} > + u.username).join(", ")} > {({ onMouseEnter, onMouseLeave }) => (
- +{users.length - 5} + +{users.length - 4}
)}
@@ -91,7 +91,7 @@ function handleClickAvatar(event: React.MouseEvent) { export default definePlugin({ name: "WhoReacted", description: "Renders the avatars of users who reacted to a message", - authors: [Devs.Ven, Devs.KannaDev], + authors: [Devs.Ven, Devs.KannaDev, Devs.newwares], patches: [{ find: ",reactionRef:", @@ -105,7 +105,19 @@ export default definePlugin({ match: /(?<=CONNECTION_OPEN:function\(\){)(\i)={}/, replace: "$&;$self.reactions=$1" } - }], + }, + { + + find: "cleanAutomaticAnchor(){", + replacement: { + match: /this\.automaticAnchor=null,this\.messageFetchAnchor=null,/, + replace: "$&$self.setScrollObj(this)," + } + } + ], + setScrollObj(scroll: any) { + Scroll = scroll; + }, renderUsers(props: RootObject) { return props.message.reactions.length > 10 ? null : ( @@ -114,9 +126,13 @@ export default definePlugin({
); }, - _renderUsers({ message, emoji, type }: RootObject) { const forceUpdate = useForceUpdater(); + React.useLayoutEffect(() => { // bc need to prevent autoscrolling + if (Scroll?.scrollCounter > 0) { + Scroll.setAutomaticAnchor(null); + } + }); React.useEffect(() => { const cb = (e: any) => { if (e.messageId === message.id) diff --git a/src/plugins/xsOverlay.desktop/index.ts b/src/plugins/xsOverlay.desktop/index.ts index 5461696ec..763f6a782 100644 --- a/src/plugins/xsOverlay.desktop/index.ts +++ b/src/plugins/xsOverlay.desktop/index.ts @@ -196,7 +196,7 @@ export default definePlugin({ if (message.mention_roles.length > 0) { for (const roleId of message.mention_roles) { - const role = GuildStore.getGuild(channel.guild_id).roles[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/preload.ts b/src/preload.ts index 08243000d..e79eb02cc 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/shared/SettingsStore.ts b/src/shared/SettingsStore.ts new file mode 100644 index 000000000..4109704bc --- /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/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 000000000..0cb1882bf --- /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/cloud.tsx b/src/utils/cloud.tsx index f56c78dc5..508b1c7ef 100644 --- a/src/utils/cloud.tsx +++ b/src/utils/cloud.tsx @@ -106,7 +106,7 @@ export async function authorizeCloud() { try { const res = await fetch(location, { - headers: new Headers({ Accept: "application/json" }) + headers: { Accept: "application/json" } }); const { secret } = await res.json(); if (secret) { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index f06df3f7c..b5c6ef559 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 { @@ -58,6 +49,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Cynosphere", id: 150745989836308480n }, + Trwy: { + name: "trey", + id: 354427199023218689n + }, Megu: { name: "Megumin", id: 545581357812678656n @@ -66,8 +61,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "botato", id: 440990343899643943n }, - obscurity: { - name: "obscurity", + fawn: { + name: "fawn", id: 336678828233588736n, }, rushii: { @@ -291,10 +286,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "RyanCaoDev", id: 952235800110694471n, }, - Strencher: { - name: "Strencher", - id: 415849376598982656n - }, FieryFlames: { name: "Fiery", id: 890228870559698955n @@ -399,6 +390,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "maisy", id: 257109471589957632n, }, + Mopi: { + name: "Mopi", + id: 1022189106614243350n + }, Grzesiek11: { name: "Grzesiek11", id: 368475654662127616n, @@ -414,6 +409,22 @@ export const Devs = /* #__PURE__*/ Object.freeze({ MrDiamond: { name: "MrDiamond", id: 523338295644782592n, + }, + Av32000: { + name: "Av32000", + id: 593436735380127770n, + }, + Kyuuhachi: { + name: "Kyuuhachi", + id: 236588665420251137n, + }, + Elvyra: { + name: "Elvyra", + id: 708275751816003615n, + }, + newwares: { + name: "newwares", + id: 421405303951851520n } } satisfies Record); diff --git a/src/utils/index.ts b/src/utils/index.ts index 90bf86082..ea4adce4a 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"; diff --git a/src/utils/quickCss.ts b/src/utils/quickCss.ts index 81320319d..99f06004c 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 ec32e2b1e..843922f2f 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); } @@ -118,10 +118,10 @@ export async function putCloudSettings(manual?: boolean) { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "PUT", - headers: new Headers({ + headers: { Authorization: await getCloudAuth(), "Content-Type": "application/octet-stream" - }), + }, body: deflateSync(new TextEncoder().encode(settings)) }); @@ -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"); @@ -162,11 +162,11 @@ export async function getCloudSettings(shouldNotify = true, force = false) { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "GET", - headers: new Headers({ + headers: { Authorization: await getCloudAuth(), Accept: "application/octet-stream", "If-None-Match": Settings.cloud.settingsSyncVersion.toString() - }), + }, }); if (res.status === 404) { @@ -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) @@ -251,9 +251,7 @@ export async function deleteCloudSettings() { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "DELETE", - headers: new Headers({ - Authorization: await getCloudAuth() - }), + headers: { Authorization: await getCloudAuth() }, }); if (!res.ok) { diff --git a/src/utils/types.ts b/src/utils/types.ts index 16867a43c..bec7cb0b3 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -17,6 +17,7 @@ */ import { Command } from "@api/Commands"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { FluxEvents } from "@webpack/types"; import { Promisable } from "type-fest"; @@ -115,6 +116,10 @@ export interface PluginDef { flux?: { [E in FluxEvents]?: (event: any) => void; }; + /** + * Allows you to manipulate context menus + */ + contextMenus?: Record; /** * Allows you to add custom actions to the Vencord Toolbox. * The key will be used as text for the button diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index d7bb5d759..24477c725 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -47,17 +47,18 @@ export let Paginator: t.Paginator; export let ScrollerThin: t.ScrollerThin; export let Clickable: t.Clickable; export let Avatar: t.Avatar; +export let FocusLock: t.FocusLock; // token lagger real /** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */ export let useToken: t.useToken; -export const MaskedLink = waitForComponent("MaskedLink", m => m?.type?.toString().includes("MASKED_LINK)")); +export const MaskedLink = waitForComponent("MaskedLink", filters.componentByCode("MASKED_LINK)")); export const Timestamp = waitForComponent("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format")); export const Flex = waitForComponent("Flex", ["Justify", "Align", "Wrap"]); export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal"); waitFor(["FormItem", "Button"], m => { - ({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar } = m); + ({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar, FocusLock } = m); Forms = m; }); diff --git a/src/webpack/common/settingsStores.ts b/src/webpack/common/settingsStores.ts index 6db21949a..4a48efda6 100644 --- a/src/webpack/common/settingsStores.ts +++ b/src/webpack/common/settingsStores.ts @@ -6,7 +6,10 @@ import { findByPropsLazy } from "@webpack"; -export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact"); -export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame"); +import * as t from "./types/settingsStores"; + + +export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact") as Record; +export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame") as Record; export const UserSettingsActionCreators = findByPropsLazy("PreloadedUserSettingsActionCreators"); diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index 0c470d6a6..f3a18d7bc 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/components.d.ts b/src/webpack/common/types/components.d.ts index b9bc434c6..3e3ffa4bd 100644 --- a/src/webpack/common/types/components.d.ts +++ b/src/webpack/common/types/components.d.ts @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import type { Moment } from "moment"; import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react"; export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; @@ -154,7 +153,7 @@ export type Switch = ComponentType>; export type Timestamp = ComponentType>; + +type FocusLock = ComponentType +}>>; diff --git a/src/webpack/common/types/index.d.ts b/src/webpack/common/types/index.d.ts index af4b5e1fb..01c968553 100644 --- a/src/webpack/common/types/index.d.ts +++ b/src/webpack/common/types/index.d.ts @@ -16,9 +16,11 @@ * along with this program. If not, see . */ +export * from "./classes"; export * from "./components"; export * from "./fluxEvents"; +export * from "./i18nMessages"; export * from "./menu"; +export * from "./settingsStores"; export * from "./stores"; export * from "./utils"; - diff --git a/src/webpack/common/types/settingsStores.ts b/src/webpack/common/types/settingsStores.ts new file mode 100644 index 000000000..5453ca352 --- /dev/null +++ b/src/webpack/common/types/settingsStores.ts @@ -0,0 +1,11 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface SettingsStore { + getSetting(): T; + updateSetting(value: T): void; + useSetting(): T; +} diff --git a/src/webpack/common/types/stores.d.ts b/src/webpack/common/types/stores.d.ts index ecc87d74c..8e89a6e20 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>; +} diff --git a/src/webpack/common/types/utils.d.ts b/src/webpack/common/types/utils.d.ts index 246659146..39af843c5 100644 --- a/src/webpack/common/types/utils.d.ts +++ b/src/webpack/common/types/utils.d.ts @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import { Guild, GuildMember } from "discord-types/general"; import type { ReactNode } from "react"; import type { FluxEvents } from "./fluxEvents"; @@ -58,6 +59,7 @@ export interface Alerts { onCancel?(): void; onConfirm?(): void; onConfirmSecondary?(): void; + onCloseCallback?(): void; }): void; /** This is a noop, it does nothing. */ close(): void; @@ -79,11 +81,7 @@ interface RestRequestData { retries?: number; } -export type RestAPI = Record<"delete" | "get" | "patch" | "post" | "put", (data: RestRequestData) => Promise> & { - V6OrEarlierAPIError: Error; - V8APIError: Error; - getAPIBaseURL(withVersion?: boolean): string; -}; +export type RestAPI = Record<"delete" | "get" | "patch" | "post" | "put", (data: RestRequestData) => Promise>; export type Permissions = "CREATE_INSTANT_INVITE" | "KICK_MEMBERS" @@ -182,3 +180,47 @@ export interface NavigationRouter { getLastRouteChangeSource(): any; getLastRouteChangeSourceLocationStack(): any; } + +export interface IconUtils { + getUserAvatarURL(user: User, canAnimate?: boolean, size?: number, format?: string): string; + getDefaultAvatarURL(id: string, discriminator?: string): string; + getUserBannerURL(data: { id: string, banner: string, canAnimate?: boolean, size: number; }): string | undefined; + getAvatarDecorationURL(dara: { avatarDecoration: string, size: number; canCanimate?: boolean; }): string | undefined; + + getGuildMemberAvatarURL(member: GuildMember, canAnimate?: string): string | null; + getGuildMemberAvatarURLSimple(data: { guildId: string, userId: string, avatar: string, canAnimate?: boolean; size?: number; }): string; + getGuildMemberBannerURL(data: { id: string, guildId: string, banner: string, canAnimate?: boolean, size: number; }): string | undefined; + + getGuildIconURL(data: { id: string, icon?: string, size?: number, canAnimate?: boolean; }): string | undefined; + getGuildBannerURL(guild: Guild, canAnimate?: boolean): string | null; + + getChannelIconURL(data: { id: string; icon?: string; applicationId?: string; size?: number; }): string | undefined; + getEmojiURL(data: { id: string, animated: boolean, size: number, forcePNG?: boolean; }): string; + + hasAnimatedGuildIcon(guild: Guild): boolean; + isAnimatedIconHash(hash: string): boolean; + + getGuildSplashURL: any; + getGuildDiscoverySplashURL: any; + getGuildHomeHeaderURL: any; + getResourceChannelIconURL: any; + getNewMemberActionIconURL: any; + getGuildTemplateIconURL: any; + getApplicationIconURL: any; + getGameAssetURL: any; + getVideoFilterAssetURL: any; + + getGuildMemberAvatarSource: any; + getUserAvatarSource: any; + getGuildSplashSource: any; + getGuildDiscoverySplashSource: any; + makeSource: any; + getGameAssetSource: any; + getGuildIconSource: any; + getGuildTemplateIconSource: any; + getGuildBannerSource: any; + getGuildHomeHeaderSource: any; + getChannelIconSource: any; + getApplicationIconSource: any; + getAnimatableSourceWithFallback: any; +} diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index c62f745a9..ec6c0e1ed 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -19,7 +19,7 @@ import type { Channel, User } from "discord-types/general"; // eslint-disable-next-line path-alias/no-relative -import { _resolveReady, filters, findByCodeLazy, findByPropsLazy, findLazy, waitFor } from "../webpack"; +import { _resolveReady, filters, findByCodeLazy, findByProps, findByPropsLazy, findLazy, proxyLazyWebpack, waitFor } from "../webpack"; import type * as t from "./types/utils"; export let FluxDispatcher: t.FluxDispatcher; @@ -37,7 +37,10 @@ export let ComponentDispatch; waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch); -export const RestAPI: t.RestAPI = findByPropsLazy("getAPIBaseURL", "get"); +export const RestAPI: t.RestAPI = proxyLazyWebpack(() => { + const mod = findByProps("getAPIBaseURL"); + return mod.HTTP ?? mod; +}); export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear"); export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight", "registerLanguage"); @@ -137,3 +140,5 @@ export const { persist: zustandPersist }: typeof import("zustand/middleware") = export const MessageActions = findByPropsLazy("editMessage", "sendMessage"); export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal"); export const InviteActions = findByPropsLazy("resolveInvite"); + +export const IconUtils: t.IconUtils = findByPropsLazy("getGuildBannerURL", "getUserAvatarURL"); diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index d65f57fcb..992bf38f3 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -60,6 +60,7 @@ export const filters = { return m => { if (filter(m)) return true; if (!m.$$typeof) return false; + if (m.type && m.type.render) return filter(m.type.render); // memo + forwardRef if (m.type) return filter(m.type); // memos if (m.render) return filter(m.render); // forwardRefs return false; @@ -83,8 +84,8 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { return true; } +let devToolsOpen = false; if (IS_DEV && IS_DISCORD_DESKTOP) { - var devToolsOpen = false; // At this point in time, DiscordNative has not been exposed yet, so setImmediate is needed setTimeout(() => { DiscordNative/* just to make sure */?.window.setDevtoolsCallbacks(() => devToolsOpen = true, () => devToolsOpen = false); @@ -475,8 +476,10 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback else if (typeof filter !== "function") throw new Error("filter must be a string, string[] or function, got " + typeof filter); - const [existing, id] = find(filter!, { isIndirect: true, isWaitFor: true }); - if (existing) return void callback(existing, id); + if (cache != null) { + const [existing, id] = find(filter, { isIndirect: true, isWaitFor: true }); + if (existing) return void callback(existing, id); + } subscriptions.set(filter, callback); } diff --git a/tsconfig.json b/tsconfig.json index 4563f3f86..e9c926408 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/**/*"] }