diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index 8bc936183..4b09463e0 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -12,6 +12,12 @@ jobs: steps: - uses: actions/checkout@v3 + if: ${{ github.event_name == 'schedule' }} + with: + ref: dev + + - uses: actions/checkout@v3 + if: ${{ github.event_name == 'workflow_dispatch' }} - uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json @@ -29,7 +35,7 @@ jobs: sudo apt-get install -y chromium-browser - name: Build web - run: pnpm buildWeb --standalone + run: pnpm buildWeb --standalone --dev - name: Create Report timeout-minutes: 10 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46d564184..a756681c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,9 +3,11 @@ on: push: branches: - main + - dev pull_request: branches: - main + - dev jobs: test: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 177e65be6..8611babd7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ The cutest Discord client mod -![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334) +| ![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334) | +|:--:| +| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) | ## Features diff --git a/package.json b/package.json index 23d9dd272..4d0dd262a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.3", + "version": "1.6.4", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs index 89cca7e47..a2e0e0024 100755 --- a/scripts/build/build.mjs +++ b/scripts/build/build.mjs @@ -21,11 +21,11 @@ import esbuild from "esbuild"; import { readdir } from "fs/promises"; import { join } from "path"; -import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isDev, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs"; const defines = { IS_STANDALONE: isStandalone, - IS_DEV: JSON.stringify(watch), + IS_DEV: JSON.stringify(isDev), IS_UPDATER_DISABLED: updaterDisabled, IS_WEB: false, IS_EXTENSION: false, diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index 353f4e060..b4c726064 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises import { join } from "path"; import Zip from "zip-local"; -import { BUILD_TIMESTAMP, commonOpts, globPlugins, VERSION, watch } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, globPlugins, isDev, VERSION } from "./common.mjs"; /** * @type {esbuild.BuildOptions} @@ -43,7 +43,7 @@ const commonOptions = { IS_WEB: "true", IS_EXTENSION: "false", IS_STANDALONE: "true", - IS_DEV: JSON.stringify(watch), + IS_DEV: JSON.stringify(isDev), IS_DISCORD_DESKTOP: "false", IS_VESKTOP: "false", IS_UPDATER_DISABLED: "true", diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index 5488b1b3b..5c34ad038 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -33,6 +33,7 @@ export const VERSION = PackageJSON.version; // https://reproducible-builds.org/docs/source-date-epoch/ export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now(); export const watch = process.argv.includes("--watch"); +export const isDev = watch || process.argv.includes("--dev"); export const isStandalone = JSON.stringify(process.argv.includes("--standalone")); export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater")); export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim(); diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 4e044c94b..540976714 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -34,7 +34,7 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) { const CANARY = process.env.USE_CANARY === "true"; const browser = await pup.launch({ - headless: true, + headless: "new", executablePath: process.env.CHROMIUM_BIN }); @@ -58,14 +58,16 @@ const report = { plugin: string; error: string; }[], - otherErrors: [] as string[] + otherErrors: [] as string[], + badWebpackFinds: [] as string[] }; const IGNORED_DISCORD_ERRORS = [ "KeybindStore: Looking for callback action", "Unable to process domain list delta: Client revision number is null", "Downloading the full bad domains file", - /\[GatewaySocket\].{0,110}Cannot access '/ + /\[GatewaySocket\].{0,110}Cannot access '/, + "search for 'name' in undefined" ] as Array; function toCodeBlock(s: string) { @@ -74,7 +76,10 @@ function toCodeBlock(s: string) { } async function printReport() { + console.log(); + console.log("# Vencord Report" + (CANARY ? " (Canary)" : "")); + console.log(); console.log("## Bad Patches"); @@ -87,12 +92,19 @@ async function printReport() { console.log(); + console.log("## Bad Webpack Finds"); + report.badWebpackFinds.forEach(p => console.log("- " + p)); + + console.log(); + console.log("## Bad Starts"); report.badStarts.forEach(p => { console.log(`- ${p.plugin}`); console.log(` - Error: ${toCodeBlock(p.error)}`); }); + console.log(); + report.otherErrors = report.otherErrors.filter(e => !IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))); console.log("## Discord Errors"); @@ -100,8 +112,9 @@ async function printReport() { console.log(`- ${toCodeBlock(e)}`); }); + console.log(); + if (process.env.DISCORD_WEBHOOK) { - // this code was written almost entirely by Copilot xD await fetch(process.env.DISCORD_WEBHOOK, { method: "POST", headers: { @@ -110,7 +123,7 @@ async function printReport() { body: JSON.stringify({ description: "Here's the latest Vencord Report!", username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""), - avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/f0204a918c6c9c9a43195997e97d8adf.webp", + avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/6101cff21e241cebb60c4a01563d0c01.webp?size=512", embeds: [ { title: "Bad Patches", @@ -125,6 +138,11 @@ async function printReport() { }).join("\n\n") || "None", color: report.badPatches.length ? 0xff0000 : 0x00ff00 }, + { + title: "Bad Webpack Finds", + description: report.badWebpackFinds.map(toCodeBlock).join("\n") || "None", + color: report.badWebpackFinds.length ? 0xff0000 : 0x00ff00 + }, { title: "Bad Starts", description: report.badStarts.map(p => { @@ -153,29 +171,35 @@ async function printReport() { page.on("console", async e => { const level = e.type(); - const args = e.args(); + const rawArgs = e.args(); - const firstArg = (await args[0]?.jsonValue()); - if (firstArg === "PUPPETEER_TEST_DONE_SIGNAL") { + const firstArg = await rawArgs[0]?.jsonValue(); + if (firstArg === "[PUPPETEER_TEST_DONE_SIGNAL]") { await browser.close(); await printReport(); process.exit(); } - const isVencord = (await args[0]?.jsonValue()) === "[Vencord]"; - const isDebug = (await args[0]?.jsonValue()) === "[PUP_DEBUG]"; + const isVencord = firstArg === "[Vencord]"; + const isDebug = firstArg === "[PUP_DEBUG]"; + const isWebpackFindFail = firstArg === "[PUP_WEBPACK_FIND_FAIL]"; + + if (isWebpackFindFail) { + process.exitCode = 1; + report.badWebpackFinds.push(await rawArgs[1].jsonValue() as string); + } if (isVencord) { - // make ci fail - process.exitCode = 1; + const args = await Promise.all(e.args().map(a => a.jsonValue())); - const jsonArgs = await Promise.all(args.map(a => a.jsonValue())); - const [, tag, message] = jsonArgs; - const cause = await maybeGetError(args[3]); + const [, tag, message] = args as Array; + const cause = await maybeGetError(e.args()[3]); switch (tag) { case "WebpackInterceptor:": - const [, plugin, type, id, regex] = (message as string).match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!; + process.exitCode = 1; + + const [, plugin, type, id, regex] = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!; report.badPatches.push({ plugin, type, @@ -183,16 +207,25 @@ page.on("console", async e => { match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"), error: cause }); + break; case "PluginManager:": - const [, name] = (message as string).match(/Failed to start (.+)/)!; + const failedToStartMatch = message.match(/Failed to start (.+)/); + if (!failedToStartMatch) break; + + process.exitCode = 1; + + const [, name] = failedToStartMatch; report.badStarts.push({ plugin: name, error: cause }); + break; } - } else if (isDebug) { + } + + if (isDebug) { console.error(e.text()); } else if (level === "error") { const text = await Promise.all( @@ -206,8 +239,8 @@ page.on("console", async e => { ).then(a => a.join(" ").trim()); - if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of")) { - console.error("Got unexpected error", text); + if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("found no module Filter:")) { + console.error("[Unexpected Error]", text); report.otherErrors.push(text); } } @@ -219,17 +252,16 @@ page.on("pageerror", e => console.error("[Page Error]", e)); await page.setBypassCSP(true); function runTime(token: string) { - console.error("[PUP_DEBUG]", "Starting test..."); + console.log("[PUP_DEBUG]", "Starting test..."); try { - // spoof languages to not be suspicious + // Spoof languages to not be suspicious Object.defineProperty(navigator, "languages", { get: function () { return ["en-US", "en"]; }, }); - // Monkey patch Logger to not log with custom css // @ts-ignore Vencord.Util.Logger.prototype._log = function (level, levelColor, args) { @@ -237,7 +269,7 @@ function runTime(token: string) { console[level]("[Vencord]", this.name + ":", ...args); }; - // force enable all plugins and patches + // Force enable all plugins and patches Vencord.Plugins.patches.length = 0; Object.values(Vencord.Plugins.plugins).forEach(p => { // Needs native server to run @@ -247,8 +279,14 @@ function runTime(token: string) { p.patches?.forEach(patch => { patch.plugin = p.name; delete patch.predicate; + if (!Array.isArray(patch.replacement)) patch.replacement = [patch.replacement]; + + patch.replacement.forEach(r => { + delete r.predicate; + }); + Vencord.Plugins.patches.push(patch); }); }); @@ -256,41 +294,137 @@ function runTime(token: string) { Vencord.Webpack.waitFor( "loginToken", m => { - console.error("[PUP_DEBUG]", "Logging in with token..."); + console.log("[PUP_DEBUG]", "Logging in with token..."); m.loginToken(token); } ); - // force load all chunks + // Force load all chunks Vencord.Webpack.onceReady.then(() => setTimeout(async () => { - console.error("[PUP_DEBUG]", "Webpack is ready!"); + console.log("[PUP_DEBUG]", "Webpack is ready!"); const { wreq } = Vencord.Webpack; - console.error("[PUP_DEBUG]", "Loading all chunks..."); - const ids = Function("return" + wreq.u.toString().match(/(?<=\()\{.+?\}/s)![0])(); - for (const id in ids) { + console.log("[PUP_DEBUG]", "Loading all chunks..."); + + let chunks = null as Record | null; + const sym = Symbol("Vencord.chunksExtract"); + + Object.defineProperty(Object.prototype, sym, { + get() { + chunks = this; + }, + set() { }, + configurable: true, + }); + + await (wreq as any).el(sym); + delete Object.prototype[sym]; + + const validChunksEntryPoints = [] as string[]; + const validChunks = [] as string[]; + const invalidChunks = [] as string[]; + + if (!chunks) throw new Error("Failed to get chunks"); + + chunksLoop: + for (const entryPoint in chunks) { + const chunkIds = chunks[entryPoint]; + + for (const id of chunkIds) { + if (!wreq.u(id)) continue; + + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); + + if (isWasm) { + invalidChunks.push(id); + continue chunksLoop; + } + + validChunks.push(id); + } + + validChunksEntryPoints.push(entryPoint); + } + + for (const entryPoint of validChunksEntryPoints) { + try { + // Loads all chunks required for an entry point + await (wreq as any).el(entryPoint); + } catch (err) { } + } + + const allChunks = Function("return " + (wreq.u.toString().match(/(?<=\()\{.+?\}/s)?.[0] ?? "null"))() as Record | null; + if (!allChunks) throw new Error("Failed to get all chunks"); + const chunksLeft = Object.keys(allChunks).filter(id => { + return !(validChunks.includes(id) || invalidChunks.includes(id)); + }); + + for (const id of chunksLeft) { const isWasm = await fetch(wreq.p + wreq.u(id)) .then(r => r.text()) .then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); - if (!isWasm) - await wreq.e(id as any); - - await new Promise(r => setTimeout(r, 150)); + // Loads a chunk + if (!isWasm) await wreq.e(id as any); } - console.error("[PUP_DEBUG]", "Finished loading chunks!"); + + // Make sure every chunk has finished loading + await new Promise(r => setTimeout(r, 1000)); + + for (const entryPoint of validChunksEntryPoints) { + try { + if (wreq.m[entryPoint]) wreq(entryPoint as any); + } catch (err) { + console.error(err); + } + } + + console.log("[PUP_DEBUG]", "Finished loading all chunks!"); for (const patch of Vencord.Plugins.patches) { if (!patch.all) { new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); } } - setTimeout(() => console.log("PUPPETEER_TEST_DONE_SIGNAL"), 1000); + + for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) { + let method = searchType; + + if (searchType === "findComponent") method = "find"; + if (searchType === "findExportedComponent") method = "findByProps"; + if (searchType === "waitFor" || searchType === "waitForComponent" || searchType === "waitForStore") { + if (typeof args[0] === "string") method = "findByProps"; + else method = "find"; + } + + try { + let result: any; + + if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") { + const [factory] = args; + result = factory(); + } else { + // @ts-ignore + result = Vencord.Webpack[method](...args); + } + + if (result == null || ("$$get" in result && result.$$get() == null)) throw "a rock at ben shapiro"; + } catch (e) { + let logMessage = searchType; + if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; + else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`; + + console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage); + } + } + + setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000); }, 1000)); } catch (e) { - console.error("[PUP_DEBUG]", "A fatal error occurred"); - console.error("[PUP_DEBUG]", e); + console.log("[PUP_DEBUG]", "A fatal error occurred:", e); process.exit(1); } } diff --git a/src/Vencord.ts b/src/Vencord.ts index 83c69e738..a106a0b7d 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -27,6 +27,8 @@ export { PlainSettings, Settings }; import "./utils/quickCss"; import "./webpack/patchWebpack"; +import { StartAt } from "@utils/types"; + import { get as dsGet } from "./api/DataStore"; import { showNotification } from "./api/Notifications"; import { PlainSettings, Settings } from "./api/Settings"; @@ -79,7 +81,7 @@ async function syncSettings() { async function init() { await onceReady; - startAllPlugins(); + startAllPlugins(StartAt.WebpackReady); syncSettings(); @@ -130,13 +132,17 @@ async function init() { } } +startAllPlugins(StartAt.Init); init(); -if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) { - document.addEventListener("DOMContentLoaded", () => { +document.addEventListener("DOMContentLoaded", () => { + startAllPlugins(StartAt.DOMContentLoaded); + + if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) { document.head.append(Object.assign(document.createElement("style"), { id: "vencord-native-titlebar-style", textContent: "[class*=titleBar]{display: none!important}" })); - }, { once: true }); -} + } +}, { once: true }); + diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index 03789ac6c..34de43c2d 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -24,9 +24,8 @@ import { proxyLazy } from "@utils/lazy"; import { Margins } from "@utils/margins"; import { classes, isObjectEmpty } from "@utils/misc"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; -import { LazyComponent } from "@utils/react"; import { OptionType, Plugin } from "@utils/types"; -import { findByCode, findByPropsLazy } from "@webpack"; +import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common"; import { User } from "discord-types/general"; import { Constructor } from "type-fest"; @@ -42,7 +41,7 @@ import { } from "./components"; import { openContributorModal } from "./ContributorModal"; -const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers")); +const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); const UserRecord: Constructor> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any; diff --git a/src/main/patchWin32Updater.ts b/src/main/patchWin32Updater.ts index ba7a9224e..96717a5ad 100644 --- a/src/main/patchWin32Updater.ts +++ b/src/main/patchWin32Updater.ts @@ -32,6 +32,8 @@ function isNewer($new: string, old: string) { } function patchLatest() { + if (process.env.DISABLE_UPDATER_AUTO_PATCHING) return; + try { const currentAppPath = dirname(process.execPath); const currentVersion = basename(currentAppPath); diff --git a/src/plugins/betterFolders/FolderSideBar.tsx b/src/plugins/betterFolders/FolderSideBar.tsx index 97959873f..53d24ed93 100644 --- a/src/plugins/betterFolders/FolderSideBar.tsx +++ b/src/plugins/betterFolders/FolderSideBar.tsx @@ -17,8 +17,7 @@ */ import ErrorBoundary from "@components/ErrorBoundary"; -import { LazyComponent } from "@utils/react"; -import { find, findByPropsLazy, findStoreLazy } from "@webpack"; +import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; import { useStateFromStores } from "@webpack/common"; import type { CSSProperties } from "react"; @@ -26,7 +25,7 @@ import { ExpandedGuildFolderStore, settings } from "."; const ChannelRTCStore = findStoreLazy("ChannelRTCStore"); const Animations = findByPropsLazy("a", "animated", "useTransition"); -const GuildsBar = LazyComponent(() => find(m => m.type?.toString().includes('("guildsnav")'))); +const GuildsBar = findComponentByCodeLazy('("guildsnav")'); export default ErrorBoundary.wrap(guildsBarProps => { const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders()); diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx index 8f40d90ff..68c50bcfd 100644 --- a/src/plugins/betterFolders/index.tsx +++ b/src/plugins/betterFolders/index.tsx @@ -18,9 +18,8 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import { proxyLazy } from "@utils/lazy"; import definePlugin, { OptionType } from "@utils/types"; -import { findByProps, findByPropsLazy, findStoreLazy } from "@webpack"; +import { findByPropsLazy, findStoreLazy } from "@webpack"; import { FluxDispatcher, i18n } from "@webpack/common"; import FolderSideBar from "./FolderSideBar"; @@ -31,7 +30,7 @@ enum FolderIconDisplay { MoreThanOneFolderExpanded } -const GuildsTree = proxyLazy(() => findByProps("GuildsTree").GuildsTree); +const { GuildsTree } = findByPropsLazy("GuildsTree"); const SortedGuildStore = findStoreLazy("SortedGuildStore"); export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore"); const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand"); diff --git a/src/plugins/clearURLs/defaultRules.ts b/src/plugins/clearURLs/defaultRules.ts index d6914b905..e7c3ecbeb 100644 --- a/src/plugins/clearURLs/defaultRules.ts +++ b/src/plugins/clearURLs/defaultRules.ts @@ -121,6 +121,9 @@ export const defaultRules = [ "t@*.twitter.com", "s@*.twitter.com", "ref_*@*.twitter.com", + "t@*.x.com", + "s@*.x.com", + "ref_*@*.x.com", "tt_medium", "tt_content", "lr@yandex.*", diff --git a/src/plugins/clientTheme/README.md b/src/plugins/clientTheme/README.md new file mode 100644 index 000000000..4b40148c5 --- /dev/null +++ b/src/plugins/clientTheme/README.md @@ -0,0 +1,7 @@ +# Classic Client Theme + +Revival of the old client theme experiment (The one that came before the sucky one that we actually got) + +![the ClientTheme theme colour picker](https://user-images.githubusercontent.com/37855219/230238053-e90b7098-373a-459a-bb8c-c24e82f69270.png) + +https://github.com/Vendicated/Vencord/assets/45497981/6c1bcb3b-e0c7-4a02-b0b8-c4c5cd954f38 diff --git a/src/plugins/clientTheme/clientTheme.css b/src/plugins/clientTheme/clientTheme.css new file mode 100644 index 000000000..023f88bd2 --- /dev/null +++ b/src/plugins/clientTheme/clientTheme.css @@ -0,0 +1,24 @@ +.client-theme-settings { + display: flex; + flex-direction: column; +} + +.client-theme-container { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.client-theme-settings-labels { + display: flex; + flex-direction: column; + justify-content: flex-start; +} + +.client-theme-container > [class^="colorSwatch"] > [class^="swatch"] { + border: thin solid var(--background-modifier-accent) !important; +} + +.client-theme-warning { + color: var(--text-danger); +} diff --git a/src/plugins/clientTheme/index.tsx b/src/plugins/clientTheme/index.tsx new file mode 100644 index 000000000..7b30863e7 --- /dev/null +++ b/src/plugins/clientTheme/index.tsx @@ -0,0 +1,214 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./clientTheme.css"; + +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import { getTheme, Theme } from "@utils/discord"; +import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; +import definePlugin, { OptionType, StartAt } from "@utils/types"; +import { findComponentByCodeLazy } from "@webpack"; +import { Button, Forms } from "@webpack/common"; + +const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR"); + +const colorPresets = [ + "#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D", + "#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42", + "#3C2E42", "#422938" +]; + +function onPickColor(color: number) { + const hexColor = color.toString(16).padStart(6, "0"); + + settings.store.color = hexColor; + updateColorVars(hexColor); +} + +function ThemeSettings() { + const lightnessWarning = hexToLightness(settings.store.color) > 45; + const lightModeWarning = getTheme() === Theme.Light; + + return ( +
+
+
+ Theme Color + Add a color to your Discord client theme +
+ +
+ {lightnessWarning || lightModeWarning + ?
+ + Your theme won't look good: + {lightnessWarning && Selected color is very light} + {lightModeWarning && Light mode isn't supported} +
+ : null + } +
+ ); +} + +const settings = definePluginSettings({ + color: { + description: "Color your Discord client theme will be based around. Light mode isn't supported", + type: OptionType.COMPONENT, + default: "313338", + component: () => + }, + resetColor: { + description: "Reset Theme Color", + type: OptionType.COMPONENT, + default: "313338", + component: () => ( + + ) + } +}); + +export default definePlugin({ + name: "ClientTheme", + authors: [Devs.F53, Devs.Nuckyz], + description: "Recreation of the old client theme experiment. Add a color to your Discord client theme", + settings, + + startAt: StartAt.DOMContentLoaded, + start() { + updateColorVars(settings.store.color); + generateColorOffsets(); + }, + + stop() { + document.getElementById("clientThemeVars")?.remove(); + document.getElementById("clientThemeOffsets")?.remove(); + } +}); + +const variableRegex = /(--primary-[5-9]\d{2}-hsl):.*?(\S*)%;/g; + +async function generateColorOffsets() { + + const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]'); + const variableLightness = {} as Record; + + // Search all stylesheets for color variables + for (const styleLinkNode of styleLinkNodes) { + const cssLink = styleLinkNode.getAttribute("href"); + if (!cssLink) continue; + + const res = await fetch(cssLink); + const cssString = await res.text(); + + // Get lightness values of --primary variables >=500 + let variableMatch = variableRegex.exec(cssString); + while (variableMatch !== null) { + const [, variable, lightness] = variableMatch; + variableLightness[variable] = parseFloat(lightness); + variableMatch = variableRegex.exec(cssString); + } + } + + // Generate offsets + const lightnessOffsets = Object.entries(variableLightness) + .map(([key, lightness]) => { + const lightnessOffset = lightness - variableLightness["--primary-600-hsl"]; + const plusOrMinus = lightnessOffset >= 0 ? "+" : "-"; + return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`; + }) + .join("\n"); + + const style = document.createElement("style"); + style.setAttribute("id", "clientThemeOffsets"); + style.textContent = `:root:root { + ${lightnessOffsets} + }`; + document.head.appendChild(style); +} + +function updateColorVars(color: string) { + const { hue, saturation, lightness } = hexToHSL(color); + + let style = document.getElementById("clientThemeVars"); + if (!style) { + style = document.createElement("style"); + style.setAttribute("id", "clientThemeVars"); + document.head.appendChild(style); + } + + style.textContent = `:root { + --theme-h: ${hue}; + --theme-s: ${saturation}%; + --theme-l: ${lightness}%; + }`; +} + +// https://css-tricks.com/converting-color-spaces-in-javascript/ +function hexToHSL(hexCode: string) { + // Hex => RGB normalized to 0-1 + const r = parseInt(hexCode.substring(0, 2), 16) / 255; + const g = parseInt(hexCode.substring(2, 4), 16) / 255; + const b = parseInt(hexCode.substring(4, 6), 16) / 255; + + // RGB => HSL + const cMax = Math.max(r, g, b); + const cMin = Math.min(r, g, b); + const delta = cMax - cMin; + + let hue: number, saturation: number, lightness: number; + + lightness = (cMax + cMin) / 2; + + if (delta === 0) { + // If r=g=b then the only thing that matters is lightness + hue = 0; + saturation = 0; + } else { + // Magic + saturation = delta / (1 - Math.abs(2 * lightness - 1)); + + if (cMax === r) + hue = ((g - b) / delta) % 6; + else if (cMax === g) + hue = (b - r) / delta + 2; + else + hue = (r - g) / delta + 4; + hue *= 60; + if (hue < 0) + hue += 360; + } + + // Move saturation and lightness from 0-1 to 0-100 + saturation *= 100; + lightness *= 100; + + return { hue, saturation, lightness }; +} + +// Minimized math just for lightness, lowers lag when changing colors +function hexToLightness(hexCode: string) { + // Hex => RGB normalized to 0-1 + const r = parseInt(hexCode.substring(0, 2), 16) / 255; + const g = parseInt(hexCode.substring(2, 4), 16) / 255; + const b = parseInt(hexCode.substring(4, 6), 16) / 255; + + const cMax = Math.max(r, g, b); + const cMin = Math.min(r, g, b); + + const lightness = 100 * ((cMax + cMin) / 2); + + return lightness; +} diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index 1c23d60e7..10853f25a 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -62,23 +62,27 @@ export default definePlugin({ } let fakeRenderWin: WeakRef | undefined; + const find = newFindWrapper(f => f); return { + ...Vencord.Webpack.Common, wp: Vencord.Webpack, wpc: Webpack.wreq.c, wreq: Webpack.wreq, wpsearch: search, wpex: extract, - wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!), - find: newFindWrapper(f => f), + wpexs: (code: string) => extract(Webpack.findModuleId(code)!), + find, findAll, findByProps: newFindWrapper(filters.byProps), findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)), findByCode: newFindWrapper(filters.byCode), findAllByCode: (code: string) => findAll(filters.byCode(code)), + findComponentByCode: newFindWrapper(filters.componentByCode), + findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)), + findExportedComponent: (...props: string[]) => find(...props)[props[0]], findStore: newFindWrapper(filters.byStoreName), PluginsApi: Vencord.Plugins, plugins: Vencord.Plugins.plugins, - React, Settings: Vencord.Settings, Api: Vencord.Api, reload: () => location.reload(), @@ -92,7 +96,25 @@ export default definePlugin({ fakeRenderWin = new WeakRef(win); win.focus(); - ReactDOM.render(React.createElement(component, props), win.document.body); + const doc = win.document; + doc.body.style.margin = "1em"; + + if (!win.prepared) { + win.prepared = true; + + [...document.querySelectorAll("style"), ...document.querySelectorAll("link[rel=stylesheet]")].forEach(s => { + const n = s.cloneNode(true) as HTMLStyleElement | HTMLLinkElement; + + if (s.parentElement?.tagName === "HEAD") + doc.head.append(n); + else if (n.id?.startsWith("vencord-") || n.id?.startsWith("vcd-")) + doc.documentElement.append(n); + else + doc.body.append(n); + }); + } + + ReactDOM.render(React.createElement(component, props), doc.body.appendChild(document.createElement("div"))); } }; }, diff --git a/src/plugins/customRPC/index.tsx b/src/plugins/customRPC/index.tsx index feed52fde..3653a0776 100644 --- a/src/plugins/customRPC/index.tsx +++ b/src/plugins/customRPC/index.tsx @@ -22,10 +22,10 @@ import { Devs } from "@utils/constants"; import { isTruthy } from "@utils/guards"; import { useAwaiter } from "@utils/react"; import definePlugin, { OptionType } from "@utils/types"; -import { findByCodeLazy, findByPropsLazy } from "@webpack"; +import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common"; -const ActivityComponent = findByCodeLazy("onOpenGameProfile"); +const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile"); const ActivityClassName = findByPropsLazy("activity", "buttonColor"); const Colors = findByPropsLazy("profileColors"); diff --git a/src/plugins/dearrow/index.tsx b/src/plugins/dearrow/index.tsx index f6b9ef952..52cf1d530 100644 --- a/src/plugins/dearrow/index.tsx +++ b/src/plugins/dearrow/index.tsx @@ -50,7 +50,7 @@ async function embedDidMount(this: Component) { const { titles, thumbnails } = await res.json(); const hasTitle = titles[0]?.votes >= 0; - const hasThumb = thumbnails[0]?.votes >= 0; + const hasThumb = thumbnails[0]?.votes >= 0 && !thumbnails[0].original; if (!hasTitle && !hasThumb) return; @@ -58,12 +58,12 @@ async function embedDidMount(this: Component) { enabled: true }; - if (titles[0]?.votes >= 0) { + if (hasTitle) { embed.dearrow.oldTitle = embed.rawTitle; embed.rawTitle = titles[0].title; } - if (thumbnails[0]?.votes >= 0 && thumbnails[0].timestamp) { + if (hasThumb) { embed.dearrow.oldThumb = embed.thumbnail.proxyURL; embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`; } diff --git a/src/plugins/experiments/index.tsx b/src/plugins/experiments/index.tsx index 89fcf33e7..f8ea4b04a 100644 --- a/src/plugins/experiments/index.tsx +++ b/src/plugins/experiments/index.tsx @@ -77,15 +77,6 @@ export default definePlugin({ } ] }, - // Fix search history being disabled / broken with isStaff - { - find: '("showNewSearch")', - predicate: () => settings.store.enableIsStaff, - replacement: { - match: /(?<=showNewSearch"\);return)\s?/, - replace: "!1&&" - } - }, { find: 'H1,title:"Experiments"', replacement: { diff --git a/src/plugins/fakeNitro/index.ts b/src/plugins/fakeNitro/index.ts index 4d6b7957b..3ac755567 100644 --- a/src/plugins/fakeNitro/index.ts +++ b/src/plugins/fakeNitro/index.ts @@ -21,10 +21,9 @@ import { definePluginSettings, Settings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies"; import { getCurrentGuild } from "@utils/discord"; -import { proxyLazy } from "@utils/lazy"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy, findStoreLazy } from "@webpack"; +import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import type { Message } from "discord-types/general"; import { applyPalette, GIFEncoder, quantize } from "gifenc"; @@ -48,9 +47,9 @@ function searchProtoClassField(localName: string, protoClass: any) { return fieldGetter?.(); } -const PreloadedUserSettingsActionCreators = proxyLazy(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators); -const AppearanceSettingsActionCreators = proxyLazy(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass)); -const ClientThemeSettingsActionsCreators = proxyLazy(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators)); +const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators); +const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass)); +const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators)); const USE_EXTERNAL_EMOJIS = 1n << 18n; const USE_EXTERNAL_STICKERS = 1n << 37n; diff --git a/src/plugins/fixImagesQuality/index.ts b/src/plugins/fixImagesQuality/index.ts new file mode 100644 index 000000000..d94c8e3b2 --- /dev/null +++ b/src/plugins/fixImagesQuality/index.ts @@ -0,0 +1,23 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "FixImagesQuality", + description: "Fixes the quality of images in the chat being horrible.", + authors: [Devs.Nuckyz], + patches: [ + { + find: "handleImageLoad=", + replacement: { + match: /(?<=getSrc\(\i\){.+?format:)\i/, + replace: "null" + } + } + ] +}); diff --git a/src/plugins/friendInvites/index.ts b/src/plugins/friendInvites/index.ts index 92209986d..e5ff447ed 100644 --- a/src/plugins/friendInvites/index.ts +++ b/src/plugins/friendInvites/index.ts @@ -23,7 +23,7 @@ import { findByPropsLazy } from "@webpack"; import { RestAPI, UserStore } from "@webpack/common"; const FriendInvites = findByPropsLazy("createFriendInvite"); -const uuid = findByPropsLazy("v4", "v1"); +const { uuid4 } = findByPropsLazy("uuid4"); export default definePlugin({ name: "FriendInvites", @@ -56,7 +56,7 @@ export default definePlugin({ let invite: any; if (uses === 1) { - const random = uuid.v4(); + const random = uuid4(); const { body: { invite_suggestions } } = await RestAPI.post({ url: "/friend-finder/find-friends", body: { diff --git a/src/plugins/gameActivityToggle/index.tsx b/src/plugins/gameActivityToggle/index.tsx index 735f124c5..2b84d26f1 100644 --- a/src/plugins/gameActivityToggle/index.tsx +++ b/src/plugins/gameActivityToggle/index.tsx @@ -20,12 +20,12 @@ import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; -import { findByCodeLazy } from "@webpack"; +import { findComponentByCodeLazy } from "@webpack"; import { StatusSettingsStores } from "@webpack/common"; import style from "./style.css?managed"; -const Button = findByCodeLazy("Button.Sizes.NONE,disabled:"); +const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:"); function makeIcon(showCurrentGame?: boolean) { return function () { diff --git a/src/plugins/gifPaste/index.ts b/src/plugins/gifPaste/index.ts index 9ce88f9d2..3e864b31a 100644 --- a/src/plugins/gifPaste/index.ts +++ b/src/plugins/gifPaste/index.ts @@ -19,11 +19,9 @@ import { Devs } from "@utils/constants"; import { insertTextIntoChatInputBox } from "@utils/discord"; import definePlugin from "@utils/types"; -import { filters, mapMangledModuleLazy } from "@webpack"; +import { findByPropsLazy } from "@webpack"; -const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', { - close: filters.byCode("activeView:null", "setState") -}); +const { closeExpressionPicker } = findByPropsLazy("closeExpressionPicker"); export default definePlugin({ name: "GifPaste", @@ -41,7 +39,7 @@ export default definePlugin({ handleSelect(gif?: { url: string; }) { if (gif) { insertTextIntoChatInputBox(gif.url + " "); - ExpressionPickerState.close(); + closeExpressionPicker(); } } }); diff --git a/src/plugins/greetStickerPicker/index.tsx b/src/plugins/greetStickerPicker/index.tsx index 2c8f33799..9623d422e 100644 --- a/src/plugins/greetStickerPicker/index.tsx +++ b/src/plugins/greetStickerPicker/index.tsx @@ -18,10 +18,9 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import { proxyLazy } from "@utils/lazy"; import definePlugin, { OptionType } from "@utils/types"; -import { findByProps, findByPropsLazy } from "@webpack"; -import { ContextMenu, FluxDispatcher, Menu } from "@webpack/common"; +import { findByPropsLazy } from "@webpack"; +import { ContextMenuApi, FluxDispatcher, Menu } from "@webpack/common"; import { Channel, Message } from "discord-types/general"; interface Sticker { @@ -51,7 +50,7 @@ const settings = definePluginSettings({ }>(); const MessageActions = findByPropsLazy("sendGreetMessage"); -const WELCOME_STICKERS = proxyLazy(() => findByProps("WELCOME_STICKERS")?.WELCOME_STICKERS); +const { WELCOME_STICKERS } = findByPropsLazy("WELCOME_STICKERS"); function greet(channel: Channel, message: Message, stickers: string[]) { const options = MessageActions.getSendMessageOptionsForReply({ @@ -184,6 +183,6 @@ export default definePlugin({ } ) { if (!(props.message as any).deleted) - ContextMenu.open(event, () => ); + ContextMenuApi.openContextMenu(event, () => ); } }); diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index 0b42a8b0b..4e747f363 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -8,7 +8,6 @@ import * as DataStore from "@api/DataStore"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import { useForceUpdater } from "@utils/react"; import definePlugin from "@utils/types"; import { findStoreLazy } from "@webpack"; import { StatusSettingsStores, Tooltip } from "webpack/common"; @@ -27,14 +26,12 @@ interface IgnoredActivity { const RunningGameStore = findStoreLazy("RunningGameStore"); function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) { - const forceUpdate = useForceUpdater(); - return ( {tooltipProps => ( diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx index 3698379c8..c40a18140 100644 --- a/src/plugins/sortFriendRequests/index.tsx +++ b/src/plugins/sortFriendRequests/index.tsx @@ -16,17 +16,27 @@ * along with this program. If not, see . */ +import { definePluginSettings } from "@api/Settings"; import { Flex } from "@components/Flex"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { RelationshipStore } from "@webpack/common"; import { User } from "discord-types/general"; -import { Settings } from "Vencord"; + +const settings = definePluginSettings({ + showDates: { + type: OptionType.BOOLEAN, + description: "Show dates on friend requests", + default: false, + restartNeeded: true + } +}); export default definePlugin({ name: "SortFriendRequests", authors: [Devs.Megu], description: "Sorts friend requests by date of receipt", + settings, patches: [{ find: "getRelationshipCounts(){", @@ -35,13 +45,11 @@ export default definePlugin({ replace: ".sortBy((row) => $self.sortList(row))" } }, { - find: "RelationshipTypes.PENDING_INCOMING?", + find: ".Messages.FRIEND_REQUEST_CANCEL", replacement: { - predicate: () => Settings.plugins.SortFriendRequests.showDates, - match: /(user:(\i),.{10,50}),subText:(\i),(className:\i\.userInfo}\))/, - replace: (_, pre, user, subtext, post) => `${pre}, - subText: $self.makeSubtext(${subtext}, ${user}), - ${post}` + predicate: () => settings.store.showDates, + match: /subText:(\i)(?=,className:\i\.userInfo}\))(?<=user:(\i).+?)/, + replace: (_, subtext, user) => `subText:$self.makeSubtext(${subtext},${user})` } }], @@ -63,14 +71,5 @@ export default definePlugin({ {!isNaN(since.getTime()) && Received — {since.toDateString()}} ); - }, - - options: { - showDates: { - type: OptionType.BOOLEAN, - description: "Show dates on friend requests", - default: false, - restartNeeded: true - } } }); diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx index 43c499ab7..f2370906b 100644 --- a/src/plugins/spotifyControls/PlayerComponent.tsx +++ b/src/plugins/spotifyControls/PlayerComponent.tsx @@ -24,7 +24,7 @@ import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons"; import { debounce } from "@utils/debounce"; import { openImageModal } from "@utils/discord"; import { classes, copyWithToast } from "@utils/misc"; -import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; +import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; import { SpotifyStore, Track } from "./SpotifyStore"; @@ -104,7 +104,7 @@ function CopyContextMenu({ name, path }: { name: string; path: string; }) { function makeContextMenu(name: string, path: string) { return (e: React.MouseEvent) => - ContextMenu.open(e, () => ); + ContextMenuApi.openContextMenu(e, () => ); } function Controls() { @@ -277,7 +277,7 @@ function Info({ track }: { track: Track; }) { alt="Album Image" onClick={() => setCoverExpanded(!coverExpanded)} onContextMenu={e => { - ContextMenu.open(e, () => ); + ContextMenuApi.openContextMenu(e, () => ); }} /> )} diff --git a/src/plugins/spotifyControls/SpotifyStore.ts b/src/plugins/spotifyControls/SpotifyStore.ts index f940d8d57..b3cd0b282 100644 --- a/src/plugins/spotifyControls/SpotifyStore.ts +++ b/src/plugins/spotifyControls/SpotifyStore.ts @@ -17,8 +17,7 @@ */ import { Settings } from "@api/Settings"; -import { proxyLazy } from "@utils/lazy"; -import { findByPropsLazy } from "@webpack"; +import { findByProps, proxyLazyWebpack } from "@webpack"; import { Flux, FluxDispatcher } from "@webpack/common"; export interface Track { @@ -66,12 +65,12 @@ interface Device { type Repeat = "off" | "track" | "context"; // Don't wanna run before Flux and Dispatcher are ready! -export const SpotifyStore = proxyLazy(() => { +export const SpotifyStore = proxyLazyWebpack(() => { // For some reason ts hates extends Flux.Store const { Store } = Flux; - const SpotifySocket = findByPropsLazy("getActiveSocketAndDevice"); - const SpotifyUtils = findByPropsLazy("SpotifyAPI"); + const SpotifySocket = findByProps("getActiveSocketAndDevice"); + const SpotifyUtils = findByProps("SpotifyAPI"); const API_BASE = "https://api.spotify.com/v1/me/player"; diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx index 9de76cacf..e35eb9b6e 100644 --- a/src/plugins/typingIndicator/index.tsx +++ b/src/plugins/typingIndicator/index.tsx @@ -19,14 +19,13 @@ import { definePluginSettings, Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import { LazyComponent } from "@utils/react"; import definePlugin, { OptionType } from "@utils/types"; -import { find, findLazy, findStoreLazy } from "@webpack"; -import { ChannelStore, GuildMemberStore, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; +import { find, findStoreLazy, LazyComponentWebpack } from "@webpack"; +import { ChannelStore, GuildMemberStore, i18n, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; import { buildSeveralUsers } from "../typingTweaks"; -const ThreeDots = LazyComponent(() => { +const ThreeDots = LazyComponentWebpack(() => { // This doesn't really need to explicitly find Dots' own module, but it's fine const res = find(m => m.Dots && !m.Menu); @@ -36,10 +35,9 @@ const ThreeDots = LazyComponent(() => { const TypingStore = findStoreLazy("TypingStore"); const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore"); -const Formatters = findLazy(m => m.Messages?.SEVERAL_USERS_TYPING); - function getDisplayName(guildId: string, userId: string) { - return GuildMemberStore.getNick(guildId, userId) ?? UserStore.getUser(userId).username; + const user = UserStore.getUser(userId); + return GuildMemberStore.getNick(guildId, userId) ?? (user as any).globalName ?? user.username; } function TypingIndicator({ channelId }: { channelId: string; }) { @@ -51,7 +49,7 @@ function TypingIndicator({ channelId }: { channelId: string; }) { const oldKeys = Object.keys(old); const currentKeys = Object.keys(current); - return oldKeys.length === currentKeys.length && JSON.stringify(oldKeys) === JSON.stringify(currentKeys); + return oldKeys.length === currentKeys.length && currentKeys.every(key => old[key] != null); } ); @@ -70,21 +68,21 @@ function TypingIndicator({ channelId }: { channelId: string; }) { switch (typingUsersArray.length) { case 0: break; case 1: { - tooltipText = Formatters.Messages.ONE_USER_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]) }); + tooltipText = i18n.Messages.ONE_USER_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]) }); break; } case 2: { - tooltipText = Formatters.Messages.TWO_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) }); + tooltipText = i18n.Messages.TWO_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) }); break; } case 3: { - tooltipText = Formatters.Messages.THREE_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) }); + tooltipText = i18n.Messages.THREE_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) }); break; } default: { tooltipText = Settings.plugins.TypingTweaks.enabled ? buildSeveralUsers({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), count: typingUsersArray.length - 2 }) - : Formatters.Messages.SEVERAL_USERS_TYPING; + : i18n.Messages.SEVERAL_USERS_TYPING; break; } } @@ -92,11 +90,10 @@ function TypingIndicator({ channelId }: { channelId: string; }) { if (typingUsersArray.length > 0) { return ( - {({ onMouseLeave, onMouseEnter }) => ( + {props => (
@@ -128,12 +125,22 @@ export default definePlugin({ settings, patches: [ + // Normal channel { - find: ".UNREAD_HIGHLIGHT", + find: "UNREAD_IMPORTANT:", replacement: { match: /channel:(\i).{0,100}?channelEmoji,.{0,250}?\.children.{0,50}?:null/, replace: "$&,$self.TypingIndicator($1.id)" } + }, + // Theads + { + // This is the thread "spine" that shows in the left + find: "M11 9H4C2.89543 9 2 8.10457 2 7V1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1V7C0 9.20914 1.79086 11 4 11H11C11.5523 11 12 10.5523 12 10C12 9.44771 11.5523 9 11 9Z", + replacement: { + match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/, + replace: "$&,$self.TypingIndicator($1.id)" + } } ], diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx index cd266c6f2..bb63a86b8 100644 --- a/src/plugins/vencordToolbox/index.tsx +++ b/src/plugins/vencordToolbox/index.tsx @@ -22,15 +22,14 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog"; import { Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import { LazyComponent } from "@utils/react"; import definePlugin from "@utils/types"; -import { filters, find } from "@webpack"; +import { filters, find, LazyComponentWebpack } from "@webpack"; import { Menu, Popout, useState } from "@webpack/common"; import type { ReactNode } from "react"; -const HeaderBarIcon = LazyComponent(() => { +const HeaderBarIcon = LazyComponentWebpack(() => { const filter = filters.byCode(".HEADER_BAR_BADGE"); - return find(m => m.Icon && filter(m.Icon)).Icon; + return find(m => m.Icon && filter(m.Icon))?.Icon; }); function VencordPopout(onClose: () => void) { diff --git a/src/plugins/voiceMessages/VoicePreview.tsx b/src/plugins/voiceMessages/VoicePreview.tsx index 912c55ae0..0976f7946 100644 --- a/src/plugins/voiceMessages/VoicePreview.tsx +++ b/src/plugins/voiceMessages/VoicePreview.tsx @@ -16,8 +16,8 @@ * along with this program. If not, see . */ -import { LazyComponent, useTimer } from "@utils/react"; -import { find } from "@webpack"; +import { useTimer } from "@utils/react"; +import { findComponentByCodeLazy } from "@webpack"; import { cl } from "./utils"; @@ -25,7 +25,7 @@ interface VoiceMessageProps { src: string; waveform: string; } -const VoiceMessage = LazyComponent(() => find(m => m.type?.toString().includes("waveform:"))); +const VoiceMessage = findComponentByCodeLazy("waveform:"); export type VoicePreviewOptions = { src?: string; diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index bbfaa6747..cf50bb86e 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -20,8 +20,8 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { saveFile } from "@utils/web"; -import { findByProps, findLazy } from "@webpack"; -import { Clipboard } from "@webpack/common"; +import { findByProps } from "@webpack"; +import { Clipboard, ComponentDispatch } from "@webpack/common"; async function fetchImage(url: string) { const res = await fetch(url); @@ -30,7 +30,6 @@ async function fetchImage(url: string) { return await res.blob(); } -const MiniDispatcher = findLazy(m => m.emitter?._events?.INSERT_TEXT); const settings = definePluginSettings({ // This needs to be all in one setting because to enable any of these, we need to make Discord use their desktop context @@ -119,11 +118,12 @@ export default definePlugin({ // Add back image context menu { find: 'navId:"image-context"', + all: true, predicate: () => settings.store.addBack, replacement: { // return IS_DESKTOP ? React.createElement(Menu, ...) - match: /return \i\.\i\?/, - replace: "return true?" + match: /return \i\.\i(?=\?|&&)/, + replace: "return true" } }, @@ -213,7 +213,7 @@ export default definePlugin({ cut() { this.copy(); - MiniDispatcher.dispatch("INSERT_TEXT", { rawText: "" }); + ComponentDispatch.dispatch("INSERT_TEXT", { rawText: "" }); }, async paste() { diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx index fb84c155a..4a2bdeeda 100644 --- a/src/plugins/whoReacted/index.tsx +++ b/src/plugins/whoReacted/index.tsx @@ -20,14 +20,14 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { sleep } from "@utils/misc"; import { Queue } from "@utils/Queue"; -import { LazyComponent, useForceUpdater } from "@utils/react"; +import { useForceUpdater } from "@utils/react"; import definePlugin from "@utils/types"; -import { findByCode, findByPropsLazy } from "@webpack"; +import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common"; import { CustomEmoji } from "@webpack/types"; import { Message, ReactionEmoji, User } from "discord-types/general"; -const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers")); +const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); const queue = new Queue(); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 383c0e30a..1e6c2a68f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -267,6 +267,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Dziurwa", id: 1001086404203389018n }, + F53: { + name: "F53", + id: 280411966126948353n + }, AutumnVN: { name: "AutumnVN", id: 393694671383166998n @@ -386,7 +390,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({ MrDiamond: { name: "MrDiamond", id: 523338295644782592n - } + }, + Korbo: { + name: "Korbo", + id: 455856406420258827n + }, } satisfies Record); // iife so #__PURE__ works correctly diff --git a/src/utils/lazy.ts b/src/utils/lazy.ts index 4bac45bc2..32336fb40 100644 --- a/src/utils/lazy.ts +++ b/src/utils/lazy.ts @@ -43,7 +43,6 @@ for (const method of [ "construct", "defineProperty", "deleteProperty", - "get", "getOwnPropertyDescriptor", "getPrototypeOf", "has", @@ -77,7 +76,7 @@ handler.getOwnPropertyDescriptor = (target, p) => { }; /** - * Wraps the result of {@see makeLazy} in a Proxy you can consume as if it wasn't lazy. + * Wraps the result of {@link makeLazy} in a Proxy you can consume as if it wasn't lazy. * On first property access, the lazy is evaluated * @param factory lazy factory * @param attempts how many times to try to evaluate the lazy before giving up @@ -86,7 +85,11 @@ handler.getOwnPropertyDescriptor = (target, p) => { * Note that the example below exists already as an api, see {@link findByPropsLazy} * @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah); */ -export function proxyLazy(factory: () => T, attempts = 5): T { +export function proxyLazy(factory: () => T, attempts = 5, isChild = false): T { + let isSameTick = true; + if (!isChild) + setTimeout(() => isSameTick = false, 0); + let tries = 0; const proxyDummy = Object.assign(function () { }, { [kCACHE]: void 0 as T | undefined, @@ -100,5 +103,21 @@ export function proxyLazy(factory: () => T, attempts = 5): T { } }); - return new Proxy(proxyDummy, handler) as any; + return new Proxy(proxyDummy, { + ...handler, + get(target, p, receiver) { + // if we're still in the same tick, it means the lazy was immediately used. + // thus, we lazy proxy the get access to make things like destructuring work as expected + // meow here will also be a lazy + // `const { meow } = findByPropsLazy("meow");` + if (!isChild && isSameTick) + return proxyLazy( + () => Reflect.get(target[kGET](), p, receiver), + attempts, + true + ); + + return Reflect.get(target[kGET](), p, receiver); + } + }) as any; } diff --git a/src/utils/lazyReact.tsx b/src/utils/lazyReact.tsx new file mode 100644 index 000000000..da36d4e79 --- /dev/null +++ b/src/utils/lazyReact.tsx @@ -0,0 +1,29 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { ComponentType } from "react"; + +import { makeLazy } from "./lazy"; + +const NoopComponent = () => null; + +/** + * A lazy component. The factory method is called on first render. + * @param factory Function returning a Component + * @param attempts How many times to try to get the component before giving up + * @returns Result of factory function + */ +export function LazyComponent(factory: () => React.ComponentType, attempts = 5) { + const get = makeLazy(factory, attempts); + const LazyComponent = (props: T) => { + const Component = get() ?? NoopComponent; + return ; + }; + + LazyComponent.$$get = get; + + return LazyComponent as ComponentType; +} diff --git a/src/utils/modal.tsx b/src/utils/modal.tsx index 1efb9cdd8..6758a1a1a 100644 --- a/src/utils/modal.tsx +++ b/src/utils/modal.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { filters, findByProps, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; +import { findByProps, findByPropsLazy } from "@webpack"; import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react"; import { LazyComponent } from "./react"; @@ -49,13 +49,7 @@ export interface ModalOptions { type RenderFunction = (props: ModalProps) => ReactNode; -export const Modals = mapMangledModuleLazy(".closeWithCircleBackground", { - ModalRoot: filters.byCode(".root"), - ModalHeader: filters.byCode(".header"), - ModalContent: filters.byCode(".content"), - ModalFooter: filters.byCode(".footerSeparator"), - ModalCloseButton: filters.byCode(".closeWithCircleBackground"), -}) as { +export const Modals = findByPropsLazy("ModalRoot", "ModalCloseButton") as { ModalRoot: ComponentType null; /** @@ -77,7 +78,6 @@ interface AwaiterOpts { * @param fallbackValue The fallback value that will be used until the promise resolved * @returns [value, error, isPending] */ - export function useAwaiter(factory: () => Promise): AwaiterRes; export function useAwaiter(factory: () => Promise, providedOpts: AwaiterOpts): AwaiterRes; export function useAwaiter(factory: () => Promise, providedOpts?: AwaiterOpts): AwaiterRes { @@ -113,31 +113,16 @@ export function useAwaiter(factory: () => Promise, providedOpts?: AwaiterO return [state.value, state.error, state.pending]; } + /** * Returns a function that can be used to force rerender react components */ - export function useForceUpdater(): () => void; export function useForceUpdater(withDep: true): [unknown, () => void]; export function useForceUpdater(withDep?: true) { const r = useReducer(x => x + 1, 0); return withDep ? r : r[1]; } -/** - * A lazy component. The factory method is called on first render. For example useful - * for const Component = LazyComponent(() => findByDisplayName("...").default) - * @param factory Function returning a Component - * @param attempts How many times to try to get the component before giving up - * @returns Result of factory function - */ - -export function LazyComponent(factory: () => React.ComponentType, attempts = 5) { - const get = makeLazy(factory, attempts); - return (props: T) => { - const Component = get() ?? NoopComponent; - return ; - }; -} interface TimerOpts { interval?: number; diff --git a/src/utils/types.ts b/src/utils/types.ts index b32b127b0..16867a43c 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -41,6 +41,8 @@ export interface Patch { all?: boolean; /** Do not warn if this patch did no changes */ noWarn?: boolean; + /** Only apply this set of replacements if all of them succeed. Use this if your replacements depend on each other */ + group?: boolean; predicate?(): boolean; } @@ -80,6 +82,11 @@ export interface PluginDef { * Whether this plugin should be enabled by default, but can be disabled */ enabledByDefault?: boolean; + /** + * When to call the start() method + * @default StartAt.WebpackReady + */ + startAt?: StartAt, /** * Optionally provide settings that the user can configure in the Plugins tab of settings. * @deprecated Use `settings` instead @@ -117,6 +124,15 @@ export interface PluginDef { tags?: string[]; } +export const enum StartAt { + /** Right away, as soon as Vencord initialised */ + Init = "Init", + /** On the DOMContentLoaded event, so once the document is ready */ + DOMContentLoaded = "DOMContentLoaded", + /** Once Discord's core webpack modules have finished loading, so as soon as things like react and flux are available */ + WebpackReady = "WebpackReady" +} + export const enum OptionType { STRING, NUMBER, diff --git a/src/webpack/common/internal.tsx b/src/webpack/common/internal.tsx index 66c52de00..9a89af362 100644 --- a/src/webpack/common/internal.tsx +++ b/src/webpack/common/internal.tsx @@ -19,9 +19,11 @@ import { LazyComponent } from "@utils/react"; // eslint-disable-next-line path-alias/no-relative -import { FilterFn, filters, waitFor } from "../webpack"; +import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack"; export function waitForComponent = React.ComponentType & Record>(name: string, filter: FilterFn | string | string[]): T { + if (IS_DEV) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]); + let myValue: T = function () { throw new Error(`Vencord could not find the ${name} Component`); } as any; @@ -30,11 +32,13 @@ export function waitForComponent = React.Comp waitFor(filter, (v: any) => { myValue = v; Object.assign(lazyComponent, v); - }); + }, { isIndirect: true }); return lazyComponent; } export function waitForStore(name: string, cb: (v: any) => void) { - waitFor(filters.byStoreName(name), cb); + if (IS_DEV) lazyWebpackSearchHistory.push(["waitForStore", [name]]); + + waitFor(filters.byStoreName(name), cb, { isIndirect: true }); } diff --git a/src/webpack/common/menu.ts b/src/webpack/common/menu.ts index c0d9c5f9d..9a4a3a799 100644 --- a/src/webpack/common/menu.ts +++ b/src/webpack/common/menu.ts @@ -17,16 +17,12 @@ */ // eslint-disable-next-line path-alias/no-relative -import { filters, mapMangledModuleLazy, waitFor } from "../webpack"; +import { findByPropsLazy, waitFor } from "../webpack"; import type * as t from "./types/menu"; export let Menu = {} as t.Menu; waitFor(["MenuItem", "MenuSliderControl"], m => Menu = m); -export const ContextMenu: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN"', { - open: filters.byCode("stopPropagation"), - openLazy: m => m.toString().length < 50, - close: filters.byCode("CONTEXT_MENU_CLOSE") -}); +export const ContextMenuApi: t.ContextMenuApi = findByPropsLazy("closeContextMenu", "openContextMenu"); diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index d10b18664..0c470d6a6 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -import { proxyLazy } from "@utils/lazy"; import type * as Stores from "discord-types/stores"; // eslint-disable-next-line path-alias/no-relative -import { filters, findByProps, findByPropsLazy, mapMangledModuleLazy } from "../webpack"; +import { findByPropsLazy } from "../webpack"; import { waitForStore } from "./internal"; import * as t from "./types/stores"; @@ -63,10 +62,6 @@ export let EmojiStore: t.EmojiStore; export let WindowStore: t.WindowStore; export let DraftStore: t.DraftStore; -export const MaskedLinkStore = mapMangledModuleLazy('"MaskedLinkStore"', { - openUntrustedLink: filters.byCode(".apply(this,arguments)") -}); - /** * React hook that returns stateful data for one or more stores * You might need a custom comparator (4th argument) if your store data is an object @@ -78,13 +73,15 @@ export const MaskedLinkStore = mapMangledModuleLazy('"MaskedLinkStore"', { * * @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id); */ -export const useStateFromStores: ( - stores: t.FluxStore[], - mapper: () => T, - idk?: any, - isEqual?: (old: T, newer: T) => boolean -) => T - = proxyLazy(() => findByProps("useStateFromStores").useStateFromStores); +export const { useStateFromStores }: { + useStateFromStores: ( + stores: t.FluxStore[], + mapper: () => T, + idk?: any, + isEqual?: (old: T, newer: T) => boolean + ) => T; +} + = findByPropsLazy("useStateFromStores"); waitForStore("DraftStore", s => DraftStore = s); waitForStore("UserStore", s => UserStore = s); diff --git a/src/webpack/common/types/menu.d.ts b/src/webpack/common/types/menu.d.ts index 29f3ffaee..0b8ab5c66 100644 --- a/src/webpack/common/types/menu.d.ts +++ b/src/webpack/common/types/menu.d.ts @@ -75,14 +75,14 @@ export interface Menu { } export interface ContextMenuApi { - close(): void; - open( + closeContextMenu(): void; + openContextMenu( event: UIEvent, render?: Menu["Menu"], options?: { enableSpellCheck?: boolean; }, renderLazy?: () => Promise ): void; - openLazy( + openContextMenuLazy( event: UIEvent, renderLazy?: () => Promise, options?: { enableSpellCheck?: boolean; } diff --git a/src/webpack/common/types/utils.d.ts b/src/webpack/common/types/utils.d.ts index 7eb5711ae..246659146 100644 --- a/src/webpack/common/types/utils.d.ts +++ b/src/webpack/common/types/utils.d.ts @@ -159,5 +159,26 @@ export interface i18n { loadPromise: Promise; - Messages: Record; + Messages: Record; +} + +export interface Clipboard { + copy(text: string): void; + SUPPORTS_COPY: boolean; +} + +export interface NavigationRouter { + back(): void; + forward(): void; + hasNavigated(): boolean; + getHistory(): { + action: string; + length: 50; + [key: string]: any; + }; + transitionTo(path: string, ...args: unknown[]): void; + transitionToGuild(guildId: string, ...args: unknown[]): void; + replaceWith(...args: unknown[]): void; + getLastRouteChangeSource(): any; + getLastRouteChangeSourceLocationStack(): any; } diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index 04df3c07d..cef4d51d6 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -16,14 +16,23 @@ * along with this program. If not, see . */ -import { proxyLazy } from "@utils/lazy"; import type { Channel, User } from "discord-types/general"; // eslint-disable-next-line path-alias/no-relative -import { _resolveReady, filters, find, findByPropsLazy, findLazy, mapMangledModuleLazy, waitFor } from "../webpack"; +import { _resolveReady, findByPropsLazy, findLazy, waitFor } from "../webpack"; import type * as t from "./types/utils"; export let FluxDispatcher: t.FluxDispatcher; + +waitFor(["dispatch", "subscribe"], m => { + FluxDispatcher = m; + const cb = () => { + m.unsubscribe("CONNECTION_OPEN", cb); + _resolveReady(); + }; + m.subscribe("CONNECTION_OPEN", cb); +}); + export let ComponentDispatch; waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch); @@ -31,7 +40,7 @@ waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m export const RestAPI: t.RestAPI = findByPropsLazy("getAPIBaseURL", "get"); export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear"); -export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight"); +export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight", "registerLanguage"); export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep"); @@ -41,7 +50,9 @@ export let SnowflakeUtils: t.SnowflakeUtils; waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m); export let Parser: t.Parser; +waitFor("parseTopic", m => Parser = m); export let Alerts: t.Alerts; +waitFor(["show", "close"], m => Alerts = m); const ToastType = { MESSAGE: 0, @@ -82,6 +93,13 @@ export const Toasts = { } }; +// This is the same module but this is easier +waitFor("showToast", m => { + Toasts.show = m.showToast; + Toasts.pop = m.popToast; +}); + + /** * Show a simple toast. If you need more options, use Toasts.show manually */ @@ -102,38 +120,12 @@ export const ApplicationAssetUtils = findByPropsLazy("fetchAssetIds", "getAssetI fetchAssetIds: (applicationId: string, e: string[]) => Promise; }; -export const Clipboard = mapMangledModuleLazy('document.queryCommandEnabled("copy")||document.queryCommandSupported("copy")', { - copy: filters.byCode(".copy("), - SUPPORTS_COPY: x => typeof x === "boolean", -}); +export const Clipboard: t.Clipboard = findByPropsLazy("SUPPORTS_COPY", "copy"); -export const NavigationRouter = mapMangledModuleLazy("transitionToGuild - ", { - transitionTo: filters.byCode("transitionTo -"), - transitionToGuild: filters.byCode("transitionToGuild -"), - goBack: filters.byCode("goBack()"), - goForward: filters.byCode("goForward()"), -}); - -waitFor(["dispatch", "subscribe"], m => { - FluxDispatcher = m; - const cb = () => { - m.unsubscribe("CONNECTION_OPEN", cb); - _resolveReady(); - }; - m.subscribe("CONNECTION_OPEN", cb); -}); - - -// This is the same module but this is easier -waitFor("showToast", m => { - Toasts.show = m.showToast; - Toasts.pop = m.popToast; -}); - -waitFor(["show", "close"], m => Alerts = m); -waitFor("parseTopic", m => Parser = m); +export const NavigationRouter: t.NavigationRouter = findByPropsLazy("transitionTo", "replaceWith", "transitionToGuild"); export let SettingsRouter: any; waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m); -export const PermissionsBits: t.PermissionsBits = proxyLazy(() => find(m => typeof m.Permissions?.ADMINISTRATOR === "bigint").Permissions); +const { Permissions } = findLazy(m => typeof m.Permissions?.ADMINISTRATOR === "bigint") as { Permissions: t.PermissionsBits; }; +export { Permissions as PermissionsBits }; diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index b81415e61..b992cfdea 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -119,12 +119,9 @@ function patchFactories(factories: Record mod.toString(); - factory.original = originalMod; - } catch { } + factory.toString = () => mod.toString(); + factory.original = originalMod; for (let i = 0; i < patches.length; i++) { const patch = patches[i]; @@ -204,6 +190,9 @@ function patchFactories(factories: Record()).add(patch.plugin); - logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); - if (IS_DEV) { - logger.debug("Function Source:\n", code); + if (newCode === code) { + if (!patch.noWarn) { + logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); + if (IS_DEV) { + logger.debug("Function Source:\n", code); + } + } + + if (patch.group) { + logger.warn(`Undoing patch ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); + code = previousCode; + mod = previousMod; + patchedBy.delete(patch.plugin); + break; } } else { code = newCode; @@ -259,9 +257,17 @@ function patchFactories(factories: Record m => - m.constructor?.displayName === name + m.constructor?.displayName === name, + + componentByCode: (...code: string[]): FilterFn => { + const filter = filters.byCode(...code); + return m => { + if (filter(m)) return true; + if (!m.$$typeof) return false; + if (m.type) return filter(m.type); // memos + if (m.render) return filter(m.render); // forwardRefs + return false; + }; + } }; export const subscriptions = new Map(); @@ -67,44 +79,6 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { if (!wreq) return false; cache = wreq.c; - - for (const id in cache) { - const { exports } = cache[id]; - if (!exports) continue; - - const numberId = Number(id); - - for (const callback of listeners) { - try { - callback(exports, numberId); - } catch (err) { - logger.error("Error in webpack listener", err); - } - } - - for (const [filter, callback] of subscriptions) { - try { - if (filter(exports)) { - subscriptions.delete(filter); - callback(exports, numberId); - } else if (typeof exports === "object") { - if (exports.default && filter(exports.default)) { - subscriptions.delete(filter); - callback(exports.default, numberId); - } - - for (const nested in exports) if (nested.length <= 3) { - if (exports[nested] && filter(exports[nested])) { - subscriptions.delete(filter); - callback(exports[nested], numberId); - } - } - } - } catch (err) { - logger.error("Error while firing callback for webpack chunk", err); - } - } - } return true; } @@ -140,20 +114,10 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn return isWaitFor ? [mod.exports, Number(key)] : mod.exports; } - if (typeof mod.exports !== "object") continue; - if (mod.exports.default && filter(mod.exports.default)) { const found = mod.exports.default; return isWaitFor ? [found, Number(key)] : found; } - - // the length check makes search about 20% faster - for (const nestedMod in mod.exports) if (nestedMod.length <= 3) { - const nested = mod.exports[nestedMod]; - if (nested && filter(nested)) { - return isWaitFor ? [nested, Number(key)] : nested; - } - } } if (!isIndirect) { @@ -163,13 +127,6 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn return isWaitFor ? [null, null] : null; }); -/** - * find but lazy - */ -export function findLazy(filter: FilterFn) { - return proxyLazy(() => find(filter)); -} - export function findAll(filter: FilterFn) { if (typeof filter !== "function") throw new Error("Invalid filter. Expected a function got " + typeof filter); @@ -181,15 +138,9 @@ export function findAll(filter: FilterFn) { if (filter(mod.exports)) ret.push(mod.exports); - else if (typeof mod.exports !== "object") - continue; if (mod.exports.default && filter(mod.exports.default)) ret.push(mod.exports.default); - else for (const nestedMod in mod.exports) if (nestedMod.length <= 3) { - const nested = mod.exports[nestedMod]; - if (nested && filter(nested)) ret.push(nested); - } } return ret; @@ -239,26 +190,12 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns break; } - if (typeof mod.exports !== "object") - continue; - if (mod.exports.default && filter(mod.exports.default)) { results[j] = mod.exports.default; filters[j] = undefined; if (++found === length) break outer; break; } - - for (const nestedMod in mod.exports) - if (nestedMod.length <= 3) { - const nested = mod.exports[nestedMod]; - if (nested && filter(nested)) { - results[j] = nested; - filters[j] = undefined; - if (++found === length) break outer; - continue outer; - } - } } } @@ -300,45 +237,47 @@ export const findModuleId = traceFunction("findModuleId", function findModuleId( return null; }); +export const lazyWebpackSearchHistory = [] as Array<["find" | "findByProps" | "findByCode" | "findStore" | "findComponent" | "findComponentByCode" | "findExportedComponent" | "waitFor" | "waitForComponent" | "waitForStore" | "proxyLazyWebpack" | "LazyComponentWebpack", any[]]>; + /** - * Finds a mangled module by the provided code "code" (must be unique and can be anywhere in the module) - * then maps it into an easily usable module via the specified mappers - * @param code Code snippet - * @param mappers Mappers to create the non mangled exports - * @returns Unmangled exports as specified in mappers + * This is just a wrapper around {@link proxyLazy} to make our reporter test for your webpack finds. * - * @example mapMangledModule("headerIdIsManaged:", { - * openModal: filters.byCode("headerIdIsManaged:"), - * closeModal: filters.byCode("key==") - * }) + * Wraps the result of {@link makeLazy} in a Proxy you can consume as if it wasn't lazy. + * On first property access, the lazy is evaluated + * @param factory lazy factory + * @param attempts how many times to try to evaluate the lazy before giving up + * @returns Proxy + * + * Note that the example below exists already as an api, see {@link findByPropsLazy} + * @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah); */ -export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string, mappers: Record): Record { - const exports = {} as Record; +export function proxyLazyWebpack(factory: () => any, attempts?: number) { + if (IS_DEV) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]); - const id = findModuleId(code); - if (id === null) - return exports; - - const mod = wreq(id); - outer: - for (const key in mod) { - const member = mod[key]; - for (const newName in mappers) { - // if the current mapper matches this module - if (mappers[newName](member)) { - exports[newName] = member; - continue outer; - } - } - } - return exports; -}); + return proxyLazy(factory, attempts); +} /** - * Same as {@link mapMangledModule} but lazy + * This is just a wrapper around {@link LazyComponent} to make our reporter test for your webpack finds. + * + * A lazy component. The factory method is called on first render. + * @param factory Function returning a Component + * @param attempts How many times to try to get the component before giving up + * @returns Result of factory function */ -export function mapMangledModuleLazy(code: string, mappers: Record): Record { - return proxyLazy(() => mapMangledModule(code, mappers)); +export function LazyComponentWebpack(factory: () => any, attempts?: number) { + if (IS_DEV) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]); + + return LazyComponent(factory, attempts); +} + +/** + * find but lazy + */ +export function findLazy(filter: FilterFn) { + if (IS_DEV) lazyWebpackSearchHistory.push(["find", [filter]]); + + return proxyLazy(() => find(filter)); } /** @@ -355,6 +294,8 @@ export function findByProps(...props: string[]) { * findByProps but lazy */ export function findByPropsLazy(...props: string[]) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findByProps", props]); + return proxyLazy(() => findByProps(...props)); } @@ -372,6 +313,8 @@ export function findByCode(...code: string[]) { * findByCode but lazy */ export function findByCodeLazy(...code: string[]) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findByCode", code]); + return proxyLazy(() => findByCode(...code)); } @@ -386,17 +329,58 @@ export function findStore(name: string) { } /** - * findByDisplayName but lazy + * findStore but lazy */ export function findStoreLazy(name: string) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findStore", [name]]); + return proxyLazy(() => findStore(name)); } +/** + * Finds the component which includes all the given code. Checks for plain components, memos and forwardRefs + */ +export function findComponentByCode(...code: string[]) { + const res = find(filters.componentByCode(...code), { isIndirect: true }); + if (!res) + handleModuleNotFound("findComponentByCode", ...code); + return res; +} + +/** + * Finds the first component that matches the filter, lazily. + */ +export function findComponentLazy(filter: FilterFn) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findComponent", [filter]]); + + return LazyComponent(() => find(filter)); +} + +/** + * Finds the first component that includes all the given code, lazily + */ +export function findComponentByCodeLazy(...code: string[]) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findComponentByCode", code]); + + return LazyComponent(() => findComponentByCode(...code)); +} + +/** + * Finds the first component that is exported by the first prop name, lazily + */ +export function findExportedComponentLazy(...props: string[]) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findExportedComponent", props]); + + return LazyComponent(() => findByProps(...props)?.[props[0]]); +} + /** * Wait for a module that matches the provided filter to be registered, * then call the callback with the module as the first argument */ -export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn) { +export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) { + if (IS_DEV && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]); + if (typeof filter === "string") filter = filters.byProps(filter); else if (Array.isArray(filter))