Merge branch 'Vendicated:main' into main

This commit is contained in:
camila 2023-11-25 17:16:00 -06:00 committed by GitHub
commit 2870bd7003
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 923 additions and 462 deletions

View file

@ -12,6 +12,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - 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 - 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 sudo apt-get install -y chromium-browser
- name: Build web - name: Build web
run: pnpm buildWeb --standalone run: pnpm buildWeb --standalone --dev
- name: Create Report - name: Create Report
timeout-minutes: 10 timeout-minutes: 10

View file

@ -3,9 +3,11 @@ on:
push: push:
branches: branches:
- main - main
- dev
pull_request: pull_request:
branches: branches:
- main - main
- dev
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -4,7 +4,9 @@
The cutest Discord client mod 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 ## Features

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.6.3", "version": "1.6.4",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {

View file

@ -21,11 +21,11 @@ import esbuild from "esbuild";
import { readdir } from "fs/promises"; import { readdir } from "fs/promises";
import { join } from "path"; 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 = { const defines = {
IS_STANDALONE: isStandalone, IS_STANDALONE: isStandalone,
IS_DEV: JSON.stringify(watch), IS_DEV: JSON.stringify(isDev),
IS_UPDATER_DISABLED: updaterDisabled, IS_UPDATER_DISABLED: updaterDisabled,
IS_WEB: false, IS_WEB: false,
IS_EXTENSION: false, IS_EXTENSION: false,

View file

@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
import { join } from "path"; import { join } from "path";
import Zip from "zip-local"; 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} * @type {esbuild.BuildOptions}
@ -43,7 +43,7 @@ const commonOptions = {
IS_WEB: "true", IS_WEB: "true",
IS_EXTENSION: "false", IS_EXTENSION: "false",
IS_STANDALONE: "true", IS_STANDALONE: "true",
IS_DEV: JSON.stringify(watch), IS_DEV: JSON.stringify(isDev),
IS_DISCORD_DESKTOP: "false", IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: "false", IS_VESKTOP: "false",
IS_UPDATER_DISABLED: "true", IS_UPDATER_DISABLED: "true",

View file

@ -33,6 +33,7 @@ export const VERSION = PackageJSON.version;
// https://reproducible-builds.org/docs/source-date-epoch/ // https://reproducible-builds.org/docs/source-date-epoch/
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now(); export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
export const watch = process.argv.includes("--watch"); 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 isStandalone = JSON.stringify(process.argv.includes("--standalone"));
export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater")); 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(); export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();

View file

@ -34,7 +34,7 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
const CANARY = process.env.USE_CANARY === "true"; const CANARY = process.env.USE_CANARY === "true";
const browser = await pup.launch({ const browser = await pup.launch({
headless: true, headless: "new",
executablePath: process.env.CHROMIUM_BIN executablePath: process.env.CHROMIUM_BIN
}); });
@ -58,14 +58,16 @@ const report = {
plugin: string; plugin: string;
error: string; error: string;
}[], }[],
otherErrors: [] as string[] otherErrors: [] as string[],
badWebpackFinds: [] as string[]
}; };
const IGNORED_DISCORD_ERRORS = [ const IGNORED_DISCORD_ERRORS = [
"KeybindStore: Looking for callback action", "KeybindStore: Looking for callback action",
"Unable to process domain list delta: Client revision number is null", "Unable to process domain list delta: Client revision number is null",
"Downloading the full bad domains file", "Downloading the full bad domains file",
/\[GatewaySocket\].{0,110}Cannot access '/ /\[GatewaySocket\].{0,110}Cannot access '/,
"search for 'name' in undefined"
] as Array<string | RegExp>; ] as Array<string | RegExp>;
function toCodeBlock(s: string) { function toCodeBlock(s: string) {
@ -74,7 +76,10 @@ function toCodeBlock(s: string) {
} }
async function printReport() { async function printReport() {
console.log();
console.log("# Vencord Report" + (CANARY ? " (Canary)" : "")); console.log("# Vencord Report" + (CANARY ? " (Canary)" : ""));
console.log(); console.log();
console.log("## Bad Patches"); console.log("## Bad Patches");
@ -87,12 +92,19 @@ async function printReport() {
console.log(); console.log();
console.log("## Bad Webpack Finds");
report.badWebpackFinds.forEach(p => console.log("- " + p));
console.log();
console.log("## Bad Starts"); console.log("## Bad Starts");
report.badStarts.forEach(p => { report.badStarts.forEach(p => {
console.log(`- ${p.plugin}`); console.log(`- ${p.plugin}`);
console.log(` - Error: ${toCodeBlock(p.error)}`); console.log(` - Error: ${toCodeBlock(p.error)}`);
}); });
console.log();
report.otherErrors = report.otherErrors.filter(e => !IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))); report.otherErrors = report.otherErrors.filter(e => !IGNORED_DISCORD_ERRORS.some(regex => e.match(regex)));
console.log("## Discord Errors"); console.log("## Discord Errors");
@ -100,8 +112,9 @@ async function printReport() {
console.log(`- ${toCodeBlock(e)}`); console.log(`- ${toCodeBlock(e)}`);
}); });
console.log();
if (process.env.DISCORD_WEBHOOK) { if (process.env.DISCORD_WEBHOOK) {
// this code was written almost entirely by Copilot xD
await fetch(process.env.DISCORD_WEBHOOK, { await fetch(process.env.DISCORD_WEBHOOK, {
method: "POST", method: "POST",
headers: { headers: {
@ -110,7 +123,7 @@ async function printReport() {
body: JSON.stringify({ body: JSON.stringify({
description: "Here's the latest Vencord Report!", description: "Here's the latest Vencord Report!",
username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""), 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: [ embeds: [
{ {
title: "Bad Patches", title: "Bad Patches",
@ -125,6 +138,11 @@ async function printReport() {
}).join("\n\n") || "None", }).join("\n\n") || "None",
color: report.badPatches.length ? 0xff0000 : 0x00ff00 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", title: "Bad Starts",
description: report.badStarts.map(p => { description: report.badStarts.map(p => {
@ -153,29 +171,35 @@ async function printReport() {
page.on("console", async e => { page.on("console", async e => {
const level = e.type(); const level = e.type();
const args = e.args(); const rawArgs = e.args();
const firstArg = (await args[0]?.jsonValue()); const firstArg = await rawArgs[0]?.jsonValue();
if (firstArg === "PUPPETEER_TEST_DONE_SIGNAL") { if (firstArg === "[PUPPETEER_TEST_DONE_SIGNAL]") {
await browser.close(); await browser.close();
await printReport(); await printReport();
process.exit(); process.exit();
} }
const isVencord = (await args[0]?.jsonValue()) === "[Vencord]"; const isVencord = firstArg === "[Vencord]";
const isDebug = (await args[0]?.jsonValue()) === "[PUP_DEBUG]"; 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) { if (isVencord) {
// make ci fail const args = await Promise.all(e.args().map(a => a.jsonValue()));
process.exitCode = 1;
const jsonArgs = await Promise.all(args.map(a => a.jsonValue())); const [, tag, message] = args as Array<string>;
const [, tag, message] = jsonArgs; const cause = await maybeGetError(e.args()[3]);
const cause = await maybeGetError(args[3]);
switch (tag) { switch (tag) {
case "WebpackInterceptor:": 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({ report.badPatches.push({
plugin, plugin,
type, type,
@ -183,16 +207,25 @@ page.on("console", async e => {
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"), match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
error: cause error: cause
}); });
break; break;
case "PluginManager:": 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({ report.badStarts.push({
plugin: name, plugin: name,
error: cause error: cause
}); });
break; break;
} }
} else if (isDebug) { }
if (isDebug) {
console.error(e.text()); console.error(e.text());
} else if (level === "error") { } else if (level === "error") {
const text = await Promise.all( const text = await Promise.all(
@ -206,8 +239,8 @@ page.on("console", async e => {
).then(a => a.join(" ").trim()); ).then(a => a.join(" ").trim());
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of")) { if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("found no module Filter:")) {
console.error("Got unexpected error", text); console.error("[Unexpected Error]", text);
report.otherErrors.push(text); report.otherErrors.push(text);
} }
} }
@ -219,17 +252,16 @@ page.on("pageerror", e => console.error("[Page Error]", e));
await page.setBypassCSP(true); await page.setBypassCSP(true);
function runTime(token: string) { function runTime(token: string) {
console.error("[PUP_DEBUG]", "Starting test..."); console.log("[PUP_DEBUG]", "Starting test...");
try { try {
// spoof languages to not be suspicious // Spoof languages to not be suspicious
Object.defineProperty(navigator, "languages", { Object.defineProperty(navigator, "languages", {
get: function () { get: function () {
return ["en-US", "en"]; return ["en-US", "en"];
}, },
}); });
// Monkey patch Logger to not log with custom css // Monkey patch Logger to not log with custom css
// @ts-ignore // @ts-ignore
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) { Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
@ -237,7 +269,7 @@ function runTime(token: string) {
console[level]("[Vencord]", this.name + ":", ...args); console[level]("[Vencord]", this.name + ":", ...args);
}; };
// force enable all plugins and patches // Force enable all plugins and patches
Vencord.Plugins.patches.length = 0; Vencord.Plugins.patches.length = 0;
Object.values(Vencord.Plugins.plugins).forEach(p => { Object.values(Vencord.Plugins.plugins).forEach(p => {
// Needs native server to run // Needs native server to run
@ -247,8 +279,14 @@ function runTime(token: string) {
p.patches?.forEach(patch => { p.patches?.forEach(patch => {
patch.plugin = p.name; patch.plugin = p.name;
delete patch.predicate; delete patch.predicate;
if (!Array.isArray(patch.replacement)) if (!Array.isArray(patch.replacement))
patch.replacement = [patch.replacement]; patch.replacement = [patch.replacement];
patch.replacement.forEach(r => {
delete r.predicate;
});
Vencord.Plugins.patches.push(patch); Vencord.Plugins.patches.push(patch);
}); });
}); });
@ -256,41 +294,137 @@ function runTime(token: string) {
Vencord.Webpack.waitFor( Vencord.Webpack.waitFor(
"loginToken", "loginToken",
m => { m => {
console.error("[PUP_DEBUG]", "Logging in with token..."); console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token); m.loginToken(token);
} }
); );
// force load all chunks // Force load all chunks
Vencord.Webpack.onceReady.then(() => setTimeout(async () => { Vencord.Webpack.onceReady.then(() => setTimeout(async () => {
console.error("[PUP_DEBUG]", "Webpack is ready!"); console.log("[PUP_DEBUG]", "Webpack is ready!");
const { wreq } = Vencord.Webpack; const { wreq } = Vencord.Webpack;
console.error("[PUP_DEBUG]", "Loading all chunks..."); console.log("[PUP_DEBUG]", "Loading all chunks...");
const ids = Function("return" + wreq.u.toString().match(/(?<=\()\{.+?\}/s)![0])();
for (const id in ids) { let chunks = null as Record<number, string[]> | 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)) const isWasm = await fetch(wreq.p + wreq.u(id))
.then(r => r.text()) .then(r => r.text())
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); .then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
if (!isWasm) if (isWasm) {
await wreq.e(id as any); invalidChunks.push(id);
continue chunksLoop;
await new Promise(r => setTimeout(r, 150));
} }
console.error("[PUP_DEBUG]", "Finished loading chunks!");
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<string | number, string[]> | 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"));
// Loads a chunk
if (!isWasm) await wreq.e(id as any);
}
// 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) { for (const patch of Vencord.Plugins.patches) {
if (!patch.all) { if (!patch.all) {
new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); 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)); }, 1000));
} catch (e) { } catch (e) {
console.error("[PUP_DEBUG]", "A fatal error occurred"); console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
console.error("[PUP_DEBUG]", e);
process.exit(1); process.exit(1);
} }
} }

View file

@ -27,6 +27,8 @@ export { PlainSettings, Settings };
import "./utils/quickCss"; import "./utils/quickCss";
import "./webpack/patchWebpack"; import "./webpack/patchWebpack";
import { StartAt } from "@utils/types";
import { get as dsGet } from "./api/DataStore"; import { get as dsGet } from "./api/DataStore";
import { showNotification } from "./api/Notifications"; import { showNotification } from "./api/Notifications";
import { PlainSettings, Settings } from "./api/Settings"; import { PlainSettings, Settings } from "./api/Settings";
@ -79,7 +81,7 @@ async function syncSettings() {
async function init() { async function init() {
await onceReady; await onceReady;
startAllPlugins(); startAllPlugins(StartAt.WebpackReady);
syncSettings(); syncSettings();
@ -130,13 +132,17 @@ async function init() {
} }
} }
startAllPlugins(StartAt.Init);
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"), { document.head.append(Object.assign(document.createElement("style"), {
id: "vencord-native-titlebar-style", id: "vencord-native-titlebar-style",
textContent: "[class*=titleBar]{display: none!important}" textContent: "[class*=titleBar]{display: none!important}"
})); }));
}, { once: true }); }
} }, { once: true });

View file

@ -24,9 +24,8 @@ import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc"; import { classes, isObjectEmpty } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { LazyComponent } from "@utils/react";
import { OptionType, Plugin } from "@utils/types"; 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 { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import { Constructor } from "type-fest"; import { Constructor } from "type-fest";
@ -42,7 +41,7 @@ import {
} from "./components"; } from "./components";
import { openContributorModal } from "./ContributorModal"; import { openContributorModal } from "./ContributorModal";
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers")); const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any; const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;

View file

@ -32,6 +32,8 @@ function isNewer($new: string, old: string) {
} }
function patchLatest() { function patchLatest() {
if (process.env.DISABLE_UPDATER_AUTO_PATCHING) return;
try { try {
const currentAppPath = dirname(process.execPath); const currentAppPath = dirname(process.execPath);
const currentVersion = basename(currentAppPath); const currentVersion = basename(currentAppPath);

View file

@ -17,8 +17,7 @@
*/ */
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { LazyComponent } from "@utils/react"; import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { find, findByPropsLazy, findStoreLazy } from "@webpack";
import { useStateFromStores } from "@webpack/common"; import { useStateFromStores } from "@webpack/common";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
@ -26,7 +25,7 @@ import { ExpandedGuildFolderStore, settings } from ".";
const ChannelRTCStore = findStoreLazy("ChannelRTCStore"); const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
const Animations = findByPropsLazy("a", "animated", "useTransition"); 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 => { export default ErrorBoundary.wrap(guildsBarProps => {
const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders()); const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders());

View file

@ -18,9 +18,8 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { proxyLazy } from "@utils/lazy";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByProps, findByPropsLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher, i18n } from "@webpack/common"; import { FluxDispatcher, i18n } from "@webpack/common";
import FolderSideBar from "./FolderSideBar"; import FolderSideBar from "./FolderSideBar";
@ -31,7 +30,7 @@ enum FolderIconDisplay {
MoreThanOneFolderExpanded MoreThanOneFolderExpanded
} }
const GuildsTree = proxyLazy(() => findByProps("GuildsTree").GuildsTree); const { GuildsTree } = findByPropsLazy("GuildsTree");
const SortedGuildStore = findStoreLazy("SortedGuildStore"); const SortedGuildStore = findStoreLazy("SortedGuildStore");
export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore"); export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand"); const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");

View file

@ -121,6 +121,9 @@ export const defaultRules = [
"t@*.twitter.com", "t@*.twitter.com",
"s@*.twitter.com", "s@*.twitter.com",
"ref_*@*.twitter.com", "ref_*@*.twitter.com",
"t@*.x.com",
"s@*.x.com",
"ref_*@*.x.com",
"tt_medium", "tt_medium",
"tt_content", "tt_content",
"lr@yandex.*", "lr@yandex.*",

View file

@ -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

View file

@ -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);
}

View file

@ -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 (
<div className="client-theme-settings">
<div className="client-theme-container">
<div className="client-theme-settings-labels">
<Forms.FormTitle tag="h3">Theme Color</Forms.FormTitle>
<Forms.FormText>Add a color to your Discord client theme</Forms.FormText>
</div>
<ColorPicker
color={parseInt(settings.store.color, 16)}
onChange={onPickColor}
showEyeDropper={false}
suggestedColors={colorPresets}
/>
</div>
{lightnessWarning || lightModeWarning
? <div>
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
<Forms.FormText className="client-theme-warning">Your theme won't look good:</Forms.FormText>
{lightnessWarning && <Forms.FormText className="client-theme-warning">Selected color is very light</Forms.FormText>}
{lightModeWarning && <Forms.FormText className="client-theme-warning">Light mode isn't supported</Forms.FormText>}
</div>
: null
}
</div>
);
}
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: () => <ThemeSettings />
},
resetColor: {
description: "Reset Theme Color",
type: OptionType.COMPONENT,
default: "313338",
component: () => (
<Button onClick={() => onPickColor(0x313338)}>
Reset Theme Color
</Button>
)
}
});
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<string, number>;
// 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;
}

View file

@ -62,23 +62,27 @@ export default definePlugin({
} }
let fakeRenderWin: WeakRef<Window> | undefined; let fakeRenderWin: WeakRef<Window> | undefined;
const find = newFindWrapper(f => f);
return { return {
...Vencord.Webpack.Common,
wp: Vencord.Webpack, wp: Vencord.Webpack,
wpc: Webpack.wreq.c, wpc: Webpack.wreq.c,
wreq: Webpack.wreq, wreq: Webpack.wreq,
wpsearch: search, wpsearch: search,
wpex: extract, wpex: extract,
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!), wpexs: (code: string) => extract(Webpack.findModuleId(code)!),
find: newFindWrapper(f => f), find,
findAll, findAll,
findByProps: newFindWrapper(filters.byProps), findByProps: newFindWrapper(filters.byProps),
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)), findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
findByCode: newFindWrapper(filters.byCode), findByCode: newFindWrapper(filters.byCode),
findAllByCode: (code: string) => findAll(filters.byCode(code)), 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), findStore: newFindWrapper(filters.byStoreName),
PluginsApi: Vencord.Plugins, PluginsApi: Vencord.Plugins,
plugins: Vencord.Plugins.plugins, plugins: Vencord.Plugins.plugins,
React,
Settings: Vencord.Settings, Settings: Vencord.Settings,
Api: Vencord.Api, Api: Vencord.Api,
reload: () => location.reload(), reload: () => location.reload(),
@ -92,7 +96,25 @@ export default definePlugin({
fakeRenderWin = new WeakRef(win); fakeRenderWin = new WeakRef(win);
win.focus(); 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")));
} }
}; };
}, },

View file

@ -22,10 +22,10 @@ import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards"; import { isTruthy } from "@utils/guards";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; 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"; 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 ActivityClassName = findByPropsLazy("activity", "buttonColor");
const Colors = findByPropsLazy("profileColors"); const Colors = findByPropsLazy("profileColors");

View file

@ -50,7 +50,7 @@ async function embedDidMount(this: Component<Props>) {
const { titles, thumbnails } = await res.json(); const { titles, thumbnails } = await res.json();
const hasTitle = titles[0]?.votes >= 0; 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; if (!hasTitle && !hasThumb) return;
@ -58,12 +58,12 @@ async function embedDidMount(this: Component<Props>) {
enabled: true enabled: true
}; };
if (titles[0]?.votes >= 0) { if (hasTitle) {
embed.dearrow.oldTitle = embed.rawTitle; embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title; embed.rawTitle = titles[0].title;
} }
if (thumbnails[0]?.votes >= 0 && thumbnails[0].timestamp) { if (hasThumb) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL; embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`; embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
} }

View file

@ -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"', find: 'H1,title:"Experiments"',
replacement: { replacement: {

View file

@ -21,10 +21,9 @@ import { definePluginSettings, Settings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies"; import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
import { getCurrentGuild } from "@utils/discord"; import { getCurrentGuild } from "@utils/discord";
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; 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 { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { Message } from "discord-types/general"; import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc"; import { applyPalette, GIFEncoder, quantize } from "gifenc";
@ -48,9 +47,9 @@ function searchProtoClassField(localName: string, protoClass: any) {
return fieldGetter?.(); return fieldGetter?.();
} }
const PreloadedUserSettingsActionCreators = proxyLazy(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators); const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators);
const AppearanceSettingsActionCreators = proxyLazy(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass)); const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));
const ClientThemeSettingsActionsCreators = proxyLazy(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators)); const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));
const USE_EXTERNAL_EMOJIS = 1n << 18n; const USE_EXTERNAL_EMOJIS = 1n << 18n;
const USE_EXTERNAL_STICKERS = 1n << 37n; const USE_EXTERNAL_STICKERS = 1n << 37n;

View file

@ -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"
}
}
]
});

View file

@ -23,7 +23,7 @@ import { findByPropsLazy } from "@webpack";
import { RestAPI, UserStore } from "@webpack/common"; import { RestAPI, UserStore } from "@webpack/common";
const FriendInvites = findByPropsLazy("createFriendInvite"); const FriendInvites = findByPropsLazy("createFriendInvite");
const uuid = findByPropsLazy("v4", "v1"); const { uuid4 } = findByPropsLazy("uuid4");
export default definePlugin({ export default definePlugin({
name: "FriendInvites", name: "FriendInvites",
@ -56,7 +56,7 @@ export default definePlugin({
let invite: any; let invite: any;
if (uses === 1) { if (uses === 1) {
const random = uuid.v4(); const random = uuid4();
const { body: { invite_suggestions } } = await RestAPI.post({ const { body: { invite_suggestions } } = await RestAPI.post({
url: "/friend-finder/find-friends", url: "/friend-finder/find-friends",
body: { body: {

View file

@ -20,12 +20,12 @@ import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByCodeLazy } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import { StatusSettingsStores } from "@webpack/common"; import { StatusSettingsStores } from "@webpack/common";
import style from "./style.css?managed"; import style from "./style.css?managed";
const Button = findByCodeLazy("Button.Sizes.NONE,disabled:"); const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:");
function makeIcon(showCurrentGame?: boolean) { function makeIcon(showCurrentGame?: boolean) {
return function () { return function () {

View file

@ -19,11 +19,9 @@
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { insertTextIntoChatInputBox } from "@utils/discord"; import { insertTextIntoChatInputBox } from "@utils/discord";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { filters, mapMangledModuleLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', { const { closeExpressionPicker } = findByPropsLazy("closeExpressionPicker");
close: filters.byCode("activeView:null", "setState")
});
export default definePlugin({ export default definePlugin({
name: "GifPaste", name: "GifPaste",
@ -41,7 +39,7 @@ export default definePlugin({
handleSelect(gif?: { url: string; }) { handleSelect(gif?: { url: string; }) {
if (gif) { if (gif) {
insertTextIntoChatInputBox(gif.url + " "); insertTextIntoChatInputBox(gif.url + " ");
ExpressionPickerState.close(); closeExpressionPicker();
} }
} }
}); });

View file

@ -18,10 +18,9 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { proxyLazy } from "@utils/lazy";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByProps, findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { ContextMenu, FluxDispatcher, Menu } from "@webpack/common"; import { ContextMenuApi, FluxDispatcher, Menu } from "@webpack/common";
import { Channel, Message } from "discord-types/general"; import { Channel, Message } from "discord-types/general";
interface Sticker { interface Sticker {
@ -51,7 +50,7 @@ const settings = definePluginSettings({
}>(); }>();
const MessageActions = findByPropsLazy("sendGreetMessage"); 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[]) { function greet(channel: Channel, message: Message, stickers: string[]) {
const options = MessageActions.getSendMessageOptionsForReply({ const options = MessageActions.getSendMessageOptionsForReply({
@ -184,6 +183,6 @@ export default definePlugin({
} }
) { ) {
if (!(props.message as any).deleted) if (!(props.message as any).deleted)
ContextMenu.open(event, () => <GreetMenu {...props} />); ContextMenuApi.openContextMenu(event, () => <GreetMenu {...props} />);
} }
}); });

View file

@ -8,7 +8,6 @@ import * as DataStore from "@api/DataStore";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findStoreLazy } from "@webpack"; import { findStoreLazy } from "@webpack";
import { StatusSettingsStores, Tooltip } from "webpack/common"; import { StatusSettingsStores, Tooltip } from "webpack/common";
@ -27,14 +26,12 @@ interface IgnoredActivity {
const RunningGameStore = findStoreLazy("RunningGameStore"); const RunningGameStore = findStoreLazy("RunningGameStore");
function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) { function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) {
const forceUpdate = useForceUpdater();
return ( return (
<Tooltip text={tooltipText}> <Tooltip text={tooltipText}>
{tooltipProps => ( {tooltipProps => (
<button <button
{...tooltipProps} {...tooltipProps}
onClick={e => handleActivityToggle(e, activity, forceUpdate)} onClick={e => handleActivityToggle(e, activity)}
style={{ all: "unset", cursor: "pointer", display: "flex", justifyContent: "center", alignItems: "center" }} style={{ all: "unset", cursor: "pointer", display: "flex", justifyContent: "center", alignItems: "center" }}
> >
<svg <svg
@ -54,11 +51,14 @@ const ToggleIconOn = (activity: IgnoredActivity, fill: string) => ToggleIcon(act
const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Enable Activity", "m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z", fill); const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Enable Activity", "m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z", fill);
function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) { function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
if (getIgnoredActivities().some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)"); const s = settings.use(["ignoredActivities"]);
const { ignoredActivities = [] } = s;
if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)"); return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)");
} }
function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity, forceUpdateButton: () => void) { function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity) {
e.stopPropagation(); e.stopPropagation();
const ignoredActivityIndex = getIgnoredActivities().findIndex(act => act.id === activity.id); const ignoredActivityIndex = getIgnoredActivities().findIndex(act => act.id === activity.id);
@ -67,7 +67,6 @@ function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>
// Trigger activities recalculation // Trigger activities recalculation
StatusSettingsStores.ShowCurrentGame.updateSetting(old => old); StatusSettingsStores.ShowCurrentGame.updateSetting(old => old);
forceUpdateButton();
} }
const settings = definePluginSettings({}).withPrivateSettings<{ const settings = definePluginSettings({}).withPrivateSettings<{
@ -90,8 +89,8 @@ export default definePlugin({
find: '.displayName="LocalActivityStore"', find: '.displayName="LocalActivityStore"',
replacement: [ replacement: [
{ {
match: /LISTENING.+?}\),(?<=(\i)\.push.+?)/, match: /HANG_STATUS.+?(?=!\i\(\i,\i\)&&)(?<=(\i)\.push.+?)/,
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored),` replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
} }
] ]
}, },

View file

@ -23,7 +23,7 @@ import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { ContextMenu, Menu, React, ReactDOM } from "@webpack/common"; import { ContextMenuApi, Menu, React, ReactDOM } from "@webpack/common";
import type { Root } from "react-dom/client"; import type { Root } from "react-dom/client";
import { Magnifier, MagnifierProps } from "./components/Magnifier"; import { Magnifier, MagnifierProps } from "./components/Magnifier";
@ -89,7 +89,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
checked={settings.store.square} checked={settings.store.square}
action={() => { action={() => {
settings.store.square = !settings.store.square; settings.store.square = !settings.store.square;
ContextMenu.close(); ContextMenuApi.closeContextMenu();
}} }}
/> />
<Menu.MenuCheckboxItem <Menu.MenuCheckboxItem
@ -98,7 +98,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
checked={settings.store.nearestNeighbour} checked={settings.store.nearestNeighbour}
action={() => { action={() => {
settings.store.nearestNeighbour = !settings.store.nearestNeighbour; settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
ContextMenu.close(); ContextMenuApi.closeContextMenu();
}} }}
/> />
<Menu.MenuControlItem <Menu.MenuControlItem

View file

@ -19,7 +19,7 @@
import { registerCommand, unregisterCommand } from "@api/Commands"; import { registerCommand, unregisterCommand } from "@api/Commands";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Patch, Plugin } from "@utils/types"; import { Patch, Plugin, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common"; import { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types"; import { FluxEvents } from "@webpack/types";
@ -85,9 +85,15 @@ for (const p of pluginsValues) {
} }
} }
export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins() { export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins(target: StartAt) {
logger.info(`Starting plugins (stage ${target})`);
for (const name in Plugins) for (const name in Plugins)
if (isPluginEnabled(name)) { if (isPluginEnabled(name)) {
const p = Plugins[name];
const startAt = p.startAt ?? StartAt.WebpackReady;
if (startAt !== target) continue;
startPlugin(Plugins[name]); startPlugin(Plugins[name]);
} }
}); });

View file

@ -22,9 +22,8 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js"; import { Devs } from "@utils/constants.js";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { Queue } from "@utils/Queue"; import { Queue } from "@utils/Queue";
import { LazyComponent } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { find, findByCode, findByPropsLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { import {
Button, Button,
ChannelStore, ChannelStore,
@ -45,9 +44,9 @@ const messageCache = new Map<string, {
fetched: boolean; fetched: boolean;
}>(); }>();
const Embed = LazyComponent(() => findByCode(".inlineMediaEmbed")); const Embed = findComponentByCodeLazy(".inlineMediaEmbed");
const AutoModEmbed = LazyComponent(() => findByCode(".withFooter]:", "childrenMessageContent:")); const AutoModEmbed = findComponentByCodeLazy(".withFooter]:", "childrenMessageContent:");
const ChannelMessage = LazyComponent(() => find(m => m.type?.toString()?.includes("renderSimpleAccessories)"))); const ChannelMessage = findComponentByCodeLazy("renderSimpleAccessories)");
const SearchResultClasses = findByPropsLazy("message", "searchResult"); const SearchResultClasses = findByPropsLazy("message", "searchResult");

View file

@ -302,6 +302,7 @@ export default definePlugin({
match: /attachments:(\i)\((\i)\)/, match: /attachments:(\i)\((\i)\)/,
replace: replace:
"attachments: $1((() => {" + "attachments: $1((() => {" +
" if ($self.shouldIgnore($2)) return $2;" +
" let old = arguments[1]?.attachments;" + " let old = arguments[1]?.attachments;" +
" if (!old) return $2;" + " if (!old) return $2;" +
" let new_ = $2.attachments?.map(a => a.id) ?? [];" + " let new_ = $2.attachments?.map(a => a.id) ?? [];" +

View file

@ -25,10 +25,6 @@ import definePlugin, { OptionType } from "@utils/types";
const EMOTE = "<:luna:1035316192220553236>"; const EMOTE = "<:luna:1035316192220553236>";
const DATA_KEY = "MessageTags_TAGS"; const DATA_KEY = "MessageTags_TAGS";
const MessageTagsMarker = Symbol("MessageTags"); const MessageTagsMarker = Symbol("MessageTags");
const author = {
id: "821472922140803112",
bot: false
};
interface Tag { interface Tag {
name: string; name: string;
@ -59,14 +55,12 @@ function createTagCommand(tag: Tag) {
execute: async (_, ctx) => { execute: async (_, ctx) => {
if (!await getTag(tag.name)) { if (!await getTag(tag.name)) {
sendBotMessage(ctx.channel.id, { sendBotMessage(ctx.channel.id, {
author,
content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)` content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)`
}); });
return { content: `/${tag.name}` }; return { content: `/${tag.name}` };
} }
if (Settings.plugins.MessageTags.clyde) sendBotMessage(ctx.channel.id, { if (Settings.plugins.MessageTags.clyde) sendBotMessage(ctx.channel.id, {
author,
content: `${EMOTE} The tag **${tag.name}** has been sent!` content: `${EMOTE} The tag **${tag.name}** has been sent!`
}); });
return { content: tag.message.replaceAll("\\n", "\n") }; return { content: tag.message.replaceAll("\\n", "\n") };
@ -162,7 +156,6 @@ export default definePlugin({
if (await getTag(name)) if (await getTag(name))
return sendBotMessage(ctx.channel.id, { return sendBotMessage(ctx.channel.id, {
author,
content: `${EMOTE} A Tag with the name **${name}** already exists!` content: `${EMOTE} A Tag with the name **${name}** already exists!`
}); });
@ -176,7 +169,6 @@ export default definePlugin({
await addTag(tag); await addTag(tag);
sendBotMessage(ctx.channel.id, { sendBotMessage(ctx.channel.id, {
author,
content: `${EMOTE} Successfully created the tag **${name}**!` content: `${EMOTE} Successfully created the tag **${name}**!`
}); });
break; // end 'create' break; // end 'create'
@ -186,7 +178,6 @@ export default definePlugin({
if (!await getTag(name)) if (!await getTag(name))
return sendBotMessage(ctx.channel.id, { return sendBotMessage(ctx.channel.id, {
author,
content: `${EMOTE} A Tag with the name **${name}** does not exist!` content: `${EMOTE} A Tag with the name **${name}** does not exist!`
}); });
@ -194,14 +185,12 @@ export default definePlugin({
await removeTag(name); await removeTag(name);
sendBotMessage(ctx.channel.id, { sendBotMessage(ctx.channel.id, {
author,
content: `${EMOTE} Successfully deleted the tag **${name}**!` content: `${EMOTE} Successfully deleted the tag **${name}**!`
}); });
break; // end 'delete' break; // end 'delete'
} }
case "list": { case "list": {
sendBotMessage(ctx.channel.id, { sendBotMessage(ctx.channel.id, {
author,
embeds: [ embeds: [
{ {
// @ts-ignore // @ts-ignore
@ -224,12 +213,10 @@ export default definePlugin({
if (!tag) if (!tag)
return sendBotMessage(ctx.channel.id, { return sendBotMessage(ctx.channel.id, {
author,
content: `${EMOTE} A Tag with the name **${name}** does not exist!` content: `${EMOTE} A Tag with the name **${name}** does not exist!`
}); });
sendBotMessage(ctx.channel.id, { sendBotMessage(ctx.channel.id, {
author,
content: tag.message.replaceAll("\\n", "\n") content: tag.message.replaceAll("\\n", "\n")
}); });
break; // end 'preview' break; // end 'preview'
@ -237,7 +224,6 @@ export default definePlugin({
default: { default: {
sendBotMessage(ctx.channel.id, { sendBotMessage(ctx.channel.id, {
author,
content: "Invalid sub-command" content: "Invalid sub-command"
}); });
break; break;

View file

@ -26,15 +26,13 @@ export default definePlugin({
authors: [Devs.Ven, Devs.adryd], authors: [Devs.Ven, Devs.adryd],
start() { start() {
fetch("https://raw.githubusercontent.com/adryd325/oneko.js/5977144dce83e4d71af1de005d16e38eebeb7b72/oneko.js") fetch("https://raw.githubusercontent.com/adryd325/oneko.js/8fa8a1864aa71cd7a794d58bc139e755e96a236c/oneko.js")
.then(x => x.text()) .then(x => x.text())
.then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.gif")) .then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.gif"))
.then(eval); .then(eval);
}, },
stop() { stop() {
clearInterval(window.onekoInterval);
delete window.onekoInterval;
document.getElementById("oneko")?.remove(); document.getElementById("oneko")?.remove();
} }
}); });

View file

@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons"; import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord"; import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { ContextMenu, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common"; import { ContextMenuApi, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import type { Guild } from "discord-types/general"; import type { Guild } from "discord-types/general";
import { settings } from ".."; import { settings } from "..";
@ -111,7 +111,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })} className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
onContextMenu={e => { onContextMenu={e => {
if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role) if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role)
ContextMenu.open(e, () => ( ContextMenuApi.openContextMenu(e, () => (
<RoleContextMenu <RoleContextMenu
guild={guild} guild={guild}
roleId={permission.id!} roleId={permission.id!}
@ -194,7 +194,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
return ( return (
<Menu.Menu <Menu.Menu
navId={cl("role-context-menu")} navId={cl("role-context-menu")}
onClose={ContextMenu.close} onClose={ContextMenuApi.closeContextMenu}
aria-label="Role Options" aria-label="Role Options"
> >
<Menu.MenuItem <Menu.MenuItem

View file

@ -18,9 +18,8 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import ExpandableHeader from "@components/ExpandableHeader"; import ExpandableHeader from "@components/ExpandableHeader";
import { proxyLazy } from "@utils/lazy";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { filters, findBulk } from "@webpack"; import { filters, findBulk, proxyLazyWebpack } from "@webpack";
import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/common"; import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/common";
import type { Guild, GuildMember } from "discord-types/general"; import type { Guild, GuildMember } from "discord-types/general";
@ -36,7 +35,7 @@ interface UserPermission {
type UserPermissions = Array<UserPermission>; type UserPermissions = Array<UserPermission>;
const Classes = proxyLazy(() => { const Classes = proxyLazyWebpack(() => {
const modules = findBulk( const modules = findBulk(
filters.byProps("roles", "rolePill", "rolePillBorder"), filters.byProps("roles", "rolePill", "rolePillBorder"),
filters.byProps("roleCircle", "dotBorderBase", "dotBorderColor"), filters.byProps("roleCircle", "dotBorderBase", "dotBorderColor"),
@ -46,7 +45,7 @@ const Classes = proxyLazy(() => {
return Object.assign({}, ...modules); return Object.assign({}, ...modules);
}) as Record<"roles" | "rolePill" | "rolePillBorder" | "desaturateUserColors" | "flex" | "alignCenter" | "justifyCenter" | "svg" | "background" | "dot" | "dotBorderColor" | "roleCircle" | "dotBorderBase" | "flex" | "alignCenter" | "justifyCenter" | "wrap" | "root" | "role" | "roleRemoveButton" | "roleDot" | "roleFlowerStar" | "roleRemoveIcon" | "roleRemoveIconFocused" | "roleVerifiedIcon" | "roleName" | "roleNameOverflow" | "actionButton" | "overflowButton" | "addButton" | "addButtonIcon" | "overflowRolesPopout" | "overflowRolesPopoutArrowWrapper" | "overflowRolesPopoutArrow" | "popoutBottom" | "popoutTop" | "overflowRolesPopoutHeader" | "overflowRolesPopoutHeaderIcon" | "overflowRolesPopoutHeaderText" | "roleIcon", string>; }) as Record<"roles" | "rolePill" | "rolePillBorder" | "desaturateUserColors" | "flex" | "alignCenter" | "justifyCenter" | "svg" | "background" | "dot" | "dotBorderColor" | "roleCircle" | "dotBorderBase" | "flex" | "alignCenter" | "justifyCenter" | "wrap" | "root" | "role" | "roleRemoveButton" | "roleDot" | "roleFlowerStar" | "roleRemoveIcon" | "roleRemoveIconFocused" | "roleVerifiedIcon" | "roleName" | "roleNameOverflow" | "actionButton" | "overflowButton" | "addButton" | "addButtonIcon" | "overflowRolesPopout" | "overflowRolesPopoutArrowWrapper" | "overflowRolesPopoutArrow" | "popoutBottom" | "popoutTop" | "overflowRolesPopoutHeader" | "overflowRolesPopoutHeaderIcon" | "overflowRolesPopoutHeaderText" | "roleIcon", string>;
function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildMember: GuildMember; }) { function UserPermissionsComponent({ guild, guildMember, showBorder }: { guild: Guild; guildMember: GuildMember; showBorder: boolean; }) {
const stns = settings.use(["permissionsSortOrder"]); const stns = settings.use(["permissionsSortOrder"]);
const [rolePermissions, userPermissions] = useMemo(() => { const [rolePermissions, userPermissions] = useMemo(() => {
@ -76,7 +75,7 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM
sortUserRoles(userRoles); sortUserRoles(userRoles);
for (const [permission, bit] of Object.entries(PermissionsBits)) { for (const [permission, bit] of Object.entries(PermissionsBits)) {
for (const { permissions, colorString, position, name } of userRoles) { for (const { permissions, colorString, position } of userRoles) {
if ((permissions & bit) === bit) { if ((permissions & bit) === bit) {
userPermissions.push({ userPermissions.push({
permission: getPermissionString(permission), permission: getPermissionString(permission),
@ -133,7 +132,7 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM
{userPermissions.length > 0 && ( {userPermissions.length > 0 && (
<div className={classes(root, roles)}> <div className={classes(root, roles)}>
{userPermissions.map(({ permission, roleColor }) => ( {userPermissions.map(({ permission, roleColor }) => (
<div className={classes(role, rolePill, rolePillBorder)}> <div className={classes(role, rolePill, showBorder ? rolePillBorder : null)}>
<div className={roleRemoveButton}> <div className={roleRemoveButton}>
<span <span
className={roleCircle} className={roleCircle}

View file

@ -163,13 +163,13 @@ export default definePlugin({
{ {
find: ".popularApplicationCommandIds,", find: ".popularApplicationCommandIds,",
replacement: { replacement: {
match: /showBorder:.{0,60}}\),(?<=guild:(\i),guildMember:(\i),.+?)/, match: /showBorder:(.{0,60})}\),(?<=guild:(\i),guildMember:(\i),.+?)/,
replace: (m, guild, guildMember) => `${m}$self.UserPermissions(${guild},${guildMember}),` replace: (m, showBoder, guild, guildMember) => `${m}$self.UserPermissions(${guild},${guildMember},${showBoder}),`
} }
} }
], ],
UserPermissions: (guild: Guild, guildMember?: GuildMember) => !!guildMember && <UserPermissions guild={guild} guildMember={guildMember} />, UserPermissions: (guild: Guild, guildMember: GuildMember | undefined, showBoder: boolean) => !!guildMember && <UserPermissions guild={guild} guildMember={guildMember} showBorder={showBoder} />,
userContextMenuPatch: makeContextMenuPatch("roles", MenuItemParentType.User), userContextMenuPatch: makeContextMenuPatch("roles", MenuItemParentType.User),
channelContextMenuPatch: makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel), channelContextMenuPatch: makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel),

View file

@ -100,10 +100,10 @@ export default definePlugin({
}, },
{ {
// Fix getRowHeight's check for whether this is the DMs section // Fix getRowHeight's check for whether this is the DMs section
// section === DMS // DMS (inlined) === section
match: /===\i\.DMS&&0/, match: /(?<=getRowHeight=\(.{2,50}?)1===\i/,
// section -1 === DMS // DMS (inlined) === section - 1
replace: "-1$&" replace: "$&-1"
}, },
{ {
// Override scrollToChannel to properly account for pinned channels // Override scrollToChannel to properly account for pinned channels

View file

@ -10,14 +10,14 @@ import { classNameFactory } from "@api/Styles";
import { openImageModal, openUserProfile } from "@utils/discord"; import { openImageModal, openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ModalRoot, ModalSize, openModal } from "@utils/modal";
import { LazyComponent, useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { findByProps, findByPropsLazy } from "@webpack"; import { findByPropsLazy, findExportedComponentLazy } from "@webpack";
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, moment, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common"; import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, moment, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
import { Guild, User } from "discord-types/general"; import { Guild, User } from "discord-types/general";
const IconUtils = findByPropsLazy("getGuildBannerURL"); const IconUtils = findByPropsLazy("getGuildBannerURL");
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper"); const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
const FriendRow = LazyComponent(() => findByProps("FriendRow").FriendRow); const FriendRow = findExportedComponentLazy("FriendRow");
const cl = classNameFactory("vc-gp-"); const cl = classNameFactory("vc-gp-");

View file

@ -67,7 +67,7 @@ export default definePlugin({
createHighlighter, createHighlighter,
renderHighlighter: ({ lang, content }: { lang: string; content: string; }) => { renderHighlighter: ({ lang, content }: { lang: string; content: string; }) => {
return createHighlighter({ return createHighlighter({
lang, lang: lang?.toLowerCase(),
content, content,
isPreview: false, isPreview: false,
}); });

View file

@ -16,12 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { LazyComponent } from "@utils/react"; import { findComponentByCodeLazy, findLazy } from "@webpack";
import { findByCode, findLazy } from "@webpack";
import { i18n, useToken } from "@webpack/common"; import { i18n, useToken } from "@webpack/common";
const ColorMap = findLazy(m => m.colors?.INTERACTIVE_MUTED?.css); const ColorMap = findLazy(m => m.colors?.INTERACTIVE_MUTED?.css);
const VerifiedIconComponent = LazyComponent(() => findByCode(".CONNECTIONS_ROLE_OFFICIAL_ICON_TOOLTIP")); const VerifiedIconComponent = findComponentByCodeLazy(".CONNECTIONS_ROLE_OFFICIAL_ICON_TOOLTIP");
export function VerifiedIcon() { export function VerifiedIcon() {
const color = useToken(ColorMap.colors.INTERACTIVE_MUTED).hex(); const color = useToken(ColorMap.colors.INTERACTIVE_MUTED).hex();

View file

@ -24,15 +24,14 @@ import { Flex } from "@components/Flex";
import { CopyIcon, LinkIcon } from "@components/Icons"; import { CopyIcon, LinkIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { copyWithToast } from "@utils/misc"; import { copyWithToast } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCode, findByCodeLazy, findByPropsLazy, findStoreLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Text, Tooltip, UserProfileStore } from "@webpack/common"; import { Text, Tooltip, UserProfileStore } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import { VerifiedIcon } from "./VerifiedIcon"; import { VerifiedIcon } from "./VerifiedIcon";
const Section = LazyComponent(() => findByCode(".lastSection]:")); const Section = findComponentByCodeLazy(".lastSection", "children:");
const ThemeStore = findStoreLazy("ThemeStore"); const ThemeStore = findStoreLazy("ThemeStore");
const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl"); const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl");
const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"'); const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"');

View file

@ -18,9 +18,8 @@
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { LazyComponent } from "@utils/react";
import { formatDuration } from "@utils/text"; import { formatDuration } from "@utils/text";
import { find, findByCode, findByPropsLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy, findComponentLazy } from "@webpack";
import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from "@webpack/common"; import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from "@webpack/common";
import type { Channel } from "discord-types/general"; import type { Channel } from "discord-types/general";
@ -81,17 +80,17 @@ const enum ChannelFlags {
const ChatScrollClasses = findByPropsLazy("auto", "content", "scrollerBase"); const ChatScrollClasses = findByPropsLazy("auto", "content", "scrollerBase");
const ChatClasses = findByPropsLazy("chat", "content", "noChat", "chatContent"); const ChatClasses = findByPropsLazy("chat", "content", "noChat", "chatContent");
const ChannelBeginHeader = LazyComponent(() => findByCode(".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE")); const ChannelBeginHeader = findComponentByCodeLazy(".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE");
const TagComponent = LazyComponent(() => find(m => { const TagComponent = findComponentLazy(m => {
if (typeof m !== "function") return false; if (typeof m !== "function") return false;
const code = Function.prototype.toString.call(m); const code = Function.prototype.toString.call(m);
// Get the component which doesn't include increasedActivity // Get the component which doesn't include increasedActivity
return code.includes(".Messages.FORUM_TAG_A11Y_FILTER_BY_TAG") && !code.includes("increasedActivityPill"); return code.includes(".Messages.FORUM_TAG_A11Y_FILTER_BY_TAG") && !code.includes("increasedActivityPill");
})); });
const EmojiParser = findByPropsLazy("convertSurrogateToName"); const EmojiParser = findByPropsLazy("convertSurrogateToName");
const EmojiUtils = findByPropsLazy("getURL", "buildEmojiReactionColorsPlatformed"); const EmojiUtils = findByPropsLazy("getURL", "getEmojiColors");
const ChannelTypesToChannelNames = { const ChannelTypesToChannelNames = {
[ChannelTypes.GUILD_TEXT]: "text", [ChannelTypes.GUILD_TEXT]: "text",

View file

@ -68,7 +68,7 @@ export default definePlugin({
patches: [ patches: [
{ {
// RenderLevel defines if a channel is hidden, collapsed in category, visible, etc // RenderLevel defines if a channel is hidden, collapsed in category, visible, etc
find: ".CannotShow=", find: '"placeholder-channel-id"',
replacement: [ replacement: [
// Remove the special logic for channels we don't have access to // Remove the special logic for channels we don't have access to
{ {
@ -77,18 +77,13 @@ export default definePlugin({
}, },
// Do not check for unreads when selecting the render level if the channel is hidden // Do not check for unreads when selecting the render level if the channel is hidden
{ {
match: /(?=!1===\i.\i\.hasRelevantUnread\(this\.record\))/, match: /(?=!\(0,\i\.getHasImportantUnread\)\(this\.record\))/,
replace: "$self.isHiddenChannel(this.record)||" replace: "$self.isHiddenChannel(this.record)||"
}, },
// Make channels we dont have access to be the same level as normal ones // Make channels we dont have access to be the same level as normal ones
{ {
match: /(?<=renderLevel:(\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/, match: /(activeJoinedRelevantThreads:.{0,50}VIEW_CHANNEL.+?renderLevel:(.+?),threadIds.+?renderLevel:).+?(?=,threadIds)/g,
replace: (_, renderLevelExpression) => renderLevelExpression replace: (_, rest, defaultRenderLevel) => `${rest}${defaultRenderLevel}`
},
// Make channels we dont have access to be the same level as normal ones
{
match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(\i)\..+?(?=,)/,
replace: (_, RenderLevels) => `${RenderLevels}.Show`
}, },
// Remove permission checking for getRenderLevel function // Remove permission checking for getRenderLevel function
{ {
@ -157,7 +152,7 @@ export default definePlugin({
} }
}, },
{ {
find: ".UNREAD_HIGHLIGHT", find: "UNREAD_IMPORTANT:",
predicate: () => settings.store.showMode === ShowMode.HiddenIconWithMutedStyle, predicate: () => settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,
replacement: [ replacement: [
// Make the channel appear as muted if it's hidden // Make the channel appear as muted if it's hidden
@ -178,7 +173,7 @@ export default definePlugin({
] ]
}, },
{ {
find: ".UNREAD_HIGHLIGHT", find: "UNREAD_IMPORTANT:",
replacement: [ replacement: [
{ {
// Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden // Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden
@ -198,7 +193,7 @@ export default definePlugin({
// Hide the new version of unreads box for hidden channels // Hide the new version of unreads box for hidden channels
find: '.displayName="ChannelListUnreadsStore"', find: '.displayName="ChannelListUnreadsStore"',
replacement: { replacement: {
match: /(?<=if\(null==(\i))(?=.{0,160}?hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module match: /(?<=if\(null==(\i))(?=.{0,160}?getHasImportantUnread\)\(\i\))/g, // Global because Discord has multiple methods like that in the same module
replace: (_, channel) => `||$self.isHiddenChannel(${channel})` replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
} }
}, },
@ -206,7 +201,7 @@ export default definePlugin({
// Make the old version of unreads box not visible for hidden channels // Make the old version of unreads box not visible for hidden channels
find: "renderBottomUnread(){", find: "renderBottomUnread(){",
replacement: { replacement: {
match: /(?=&&\i\.\i\.hasRelevantUnread\((\i\.record)\))/, match: /(?=&&\(0,\i\.getHasImportantUnread\)\((\i\.record)\))/,
replace: "&&!$self.isHiddenChannel($1)" replace: "&&!$self.isHiddenChannel($1)"
} }
}, },
@ -214,7 +209,7 @@ export default definePlugin({
// Make the state of the old version of unreads box not include hidden channels // Make the state of the old version of unreads box not include hidden channels
find: ".useFlattenedChannelIdListWithThreads)", find: ".useFlattenedChannelIdListWithThreads)",
replacement: { replacement: {
match: /(?=&&\i\.\i\.hasRelevantUnread\((\i)\))/, match: /(?=&&\(0,\i\.getHasImportantUnread\)\((\i)\))/,
replace: "&&!$self.isHiddenChannel($1)" replace: "&&!$self.isHiddenChannel($1)"
} }
}, },
@ -260,7 +255,7 @@ export default definePlugin({
{ {
find: '"alt+shift+down"', find: '"alt+shift+down"',
replacement: { replacement: {
match: /(?<=getChannel\(\i\);return null!=(\i))(?=.{0,150}?hasRelevantUnread\(\i\))/, match: /(?<=getChannel\(\i\);return null!=(\i))(?=.{0,150}?getHasImportantUnread\)\(\i\))/,
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})` replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`
} }
}, },

View file

@ -16,17 +16,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { RelationshipStore } from "@webpack/common"; import { RelationshipStore } from "@webpack/common";
import { User } from "discord-types/general"; 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({ export default definePlugin({
name: "SortFriendRequests", name: "SortFriendRequests",
authors: [Devs.Megu], authors: [Devs.Megu],
description: "Sorts friend requests by date of receipt", description: "Sorts friend requests by date of receipt",
settings,
patches: [{ patches: [{
find: "getRelationshipCounts(){", find: "getRelationshipCounts(){",
@ -35,13 +45,11 @@ export default definePlugin({
replace: ".sortBy((row) => $self.sortList(row))" replace: ".sortBy((row) => $self.sortList(row))"
} }
}, { }, {
find: "RelationshipTypes.PENDING_INCOMING?", find: ".Messages.FRIEND_REQUEST_CANCEL",
replacement: { replacement: {
predicate: () => Settings.plugins.SortFriendRequests.showDates, predicate: () => settings.store.showDates,
match: /(user:(\i),.{10,50}),subText:(\i),(className:\i\.userInfo}\))/, match: /subText:(\i)(?=,className:\i\.userInfo}\))(?<=user:(\i).+?)/,
replace: (_, pre, user, subtext, post) => `${pre}, replace: (_, subtext, user) => `subText:$self.makeSubtext(${subtext},${user})`
subText: $self.makeSubtext(${subtext}, ${user}),
${post}`
} }
}], }],
@ -63,14 +71,5 @@ export default definePlugin({
{!isNaN(since.getTime()) && <span>Received &mdash; {since.toDateString()}</span>} {!isNaN(since.getTime()) && <span>Received &mdash; {since.toDateString()}</span>}
</Flex> </Flex>
); );
},
options: {
showDates: {
type: OptionType.BOOLEAN,
description: "Show dates on friend requests",
default: false,
restartNeeded: true
}
} }
}); });

View file

@ -24,7 +24,7 @@ import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
import { openImageModal } from "@utils/discord"; import { openImageModal } from "@utils/discord";
import { classes, copyWithToast } from "@utils/misc"; 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"; import { SpotifyStore, Track } from "./SpotifyStore";
@ -104,7 +104,7 @@ function CopyContextMenu({ name, path }: { name: string; path: string; }) {
function makeContextMenu(name: string, path: string) { function makeContextMenu(name: string, path: string) {
return (e: React.MouseEvent<HTMLElement, MouseEvent>) => return (e: React.MouseEvent<HTMLElement, MouseEvent>) =>
ContextMenu.open(e, () => <CopyContextMenu name={name} path={path} />); ContextMenuApi.openContextMenu(e, () => <CopyContextMenu name={name} path={path} />);
} }
function Controls() { function Controls() {
@ -277,7 +277,7 @@ function Info({ track }: { track: Track; }) {
alt="Album Image" alt="Album Image"
onClick={() => setCoverExpanded(!coverExpanded)} onClick={() => setCoverExpanded(!coverExpanded)}
onContextMenu={e => { onContextMenu={e => {
ContextMenu.open(e, () => <AlbumContextMenu track={track} />); ContextMenuApi.openContextMenu(e, () => <AlbumContextMenu track={track} />);
}} }}
/> />
)} )}

View file

@ -17,8 +17,7 @@
*/ */
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { proxyLazy } from "@utils/lazy"; import { findByProps, proxyLazyWebpack } from "@webpack";
import { findByPropsLazy } from "@webpack";
import { Flux, FluxDispatcher } from "@webpack/common"; import { Flux, FluxDispatcher } from "@webpack/common";
export interface Track { export interface Track {
@ -66,12 +65,12 @@ interface Device {
type Repeat = "off" | "track" | "context"; type Repeat = "off" | "track" | "context";
// Don't wanna run before Flux and Dispatcher are ready! // 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 // For some reason ts hates extends Flux.Store
const { Store } = Flux; const { Store } = Flux;
const SpotifySocket = findByPropsLazy("getActiveSocketAndDevice"); const SpotifySocket = findByProps("getActiveSocketAndDevice");
const SpotifyUtils = findByPropsLazy("SpotifyAPI"); const SpotifyUtils = findByProps("SpotifyAPI");
const API_BASE = "https://api.spotify.com/v1/me/player"; const API_BASE = "https://api.spotify.com/v1/me/player";

View file

@ -19,14 +19,13 @@
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { LazyComponent } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { find, findLazy, findStoreLazy } from "@webpack"; import { find, findStoreLazy, LazyComponentWebpack } from "@webpack";
import { ChannelStore, GuildMemberStore, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; import { ChannelStore, GuildMemberStore, i18n, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { buildSeveralUsers } from "../typingTweaks"; 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 // This doesn't really need to explicitly find Dots' own module, but it's fine
const res = find(m => m.Dots && !m.Menu); const res = find(m => m.Dots && !m.Menu);
@ -36,10 +35,9 @@ const ThreeDots = LazyComponent(() => {
const TypingStore = findStoreLazy("TypingStore"); const TypingStore = findStoreLazy("TypingStore");
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore"); const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
const Formatters = findLazy(m => m.Messages?.SEVERAL_USERS_TYPING);
function getDisplayName(guildId: string, userId: string) { 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; }) { function TypingIndicator({ channelId }: { channelId: string; }) {
@ -51,7 +49,7 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
const oldKeys = Object.keys(old); const oldKeys = Object.keys(old);
const currentKeys = Object.keys(current); 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) { switch (typingUsersArray.length) {
case 0: break; case 0: break;
case 1: { 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; break;
} }
case 2: { 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; break;
} }
case 3: { 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; break;
} }
default: { default: {
tooltipText = Settings.plugins.TypingTweaks.enabled tooltipText = Settings.plugins.TypingTweaks.enabled
? buildSeveralUsers({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), count: typingUsersArray.length - 2 }) ? 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; break;
} }
} }
@ -92,11 +90,10 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
if (typingUsersArray.length > 0) { if (typingUsersArray.length > 0) {
return ( return (
<Tooltip text={tooltipText!}> <Tooltip text={tooltipText!}>
{({ onMouseLeave, onMouseEnter }) => ( {props => (
<div <div
{...props}
style={{ marginLeft: 6, height: 16, display: "flex", alignItems: "center", zIndex: 0, cursor: "pointer" }} style={{ marginLeft: 6, height: 16, display: "flex", alignItems: "center", zIndex: 0, cursor: "pointer" }}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
> >
<ThreeDots dotRadius={3} themed={true} /> <ThreeDots dotRadius={3} themed={true} />
</div> </div>
@ -128,12 +125,22 @@ export default definePlugin({
settings, settings,
patches: [ patches: [
// Normal channel
{ {
find: ".UNREAD_HIGHLIGHT", find: "UNREAD_IMPORTANT:",
replacement: { replacement: {
match: /channel:(\i).{0,100}?channelEmoji,.{0,250}?\.children.{0,50}?:null/, match: /channel:(\i).{0,100}?channelEmoji,.{0,250}?\.children.{0,50}?:null/,
replace: "$&,$self.TypingIndicator($1.id)" 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)"
}
} }
], ],

View file

@ -22,15 +22,14 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { LazyComponent } from "@utils/react";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { filters, find } from "@webpack"; import { filters, find, LazyComponentWebpack } from "@webpack";
import { Menu, Popout, useState } from "@webpack/common"; import { Menu, Popout, useState } from "@webpack/common";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
const HeaderBarIcon = LazyComponent(() => { const HeaderBarIcon = LazyComponentWebpack(() => {
const filter = filters.byCode(".HEADER_BAR_BADGE"); 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) { function VencordPopout(onClose: () => void) {

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { LazyComponent, useTimer } from "@utils/react"; import { useTimer } from "@utils/react";
import { find } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import { cl } from "./utils"; import { cl } from "./utils";
@ -25,7 +25,7 @@ interface VoiceMessageProps {
src: string; src: string;
waveform: string; waveform: string;
} }
const VoiceMessage = LazyComponent<VoiceMessageProps>(() => find(m => m.type?.toString().includes("waveform:"))); const VoiceMessage = findComponentByCodeLazy<VoiceMessageProps>("waveform:");
export type VoicePreviewOptions = { export type VoicePreviewOptions = {
src?: string; src?: string;

View file

@ -20,8 +20,8 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { saveFile } from "@utils/web"; import { saveFile } from "@utils/web";
import { findByProps, findLazy } from "@webpack"; import { findByProps } from "@webpack";
import { Clipboard } from "@webpack/common"; import { Clipboard, ComponentDispatch } from "@webpack/common";
async function fetchImage(url: string) { async function fetchImage(url: string) {
const res = await fetch(url); const res = await fetch(url);
@ -30,7 +30,6 @@ async function fetchImage(url: string) {
return await res.blob(); return await res.blob();
} }
const MiniDispatcher = findLazy(m => m.emitter?._events?.INSERT_TEXT);
const settings = definePluginSettings({ 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 // 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 // Add back image context menu
{ {
find: 'navId:"image-context"', find: 'navId:"image-context"',
all: true,
predicate: () => settings.store.addBack, predicate: () => settings.store.addBack,
replacement: { replacement: {
// return IS_DESKTOP ? React.createElement(Menu, ...) // return IS_DESKTOP ? React.createElement(Menu, ...)
match: /return \i\.\i\?/, match: /return \i\.\i(?=\?|&&)/,
replace: "return true?" replace: "return true"
} }
}, },
@ -213,7 +213,7 @@ export default definePlugin({
cut() { cut() {
this.copy(); this.copy();
MiniDispatcher.dispatch("INSERT_TEXT", { rawText: "" }); ComponentDispatch.dispatch("INSERT_TEXT", { rawText: "" });
}, },
async paste() { async paste() {

View file

@ -20,14 +20,14 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { sleep } from "@utils/misc"; import { sleep } from "@utils/misc";
import { Queue } from "@utils/Queue"; import { Queue } from "@utils/Queue";
import { LazyComponent, useForceUpdater } from "@utils/react"; import { useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types"; 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 { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
import { CustomEmoji } from "@webpack/types"; import { CustomEmoji } from "@webpack/types";
import { Message, ReactionEmoji, User } from "discord-types/general"; 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 AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
const queue = new Queue(); const queue = new Queue();

View file

@ -267,6 +267,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Dziurwa", name: "Dziurwa",
id: 1001086404203389018n id: 1001086404203389018n
}, },
F53: {
name: "F53",
id: 280411966126948353n
},
AutumnVN: { AutumnVN: {
name: "AutumnVN", name: "AutumnVN",
id: 393694671383166998n id: 393694671383166998n

View file

@ -43,7 +43,6 @@ for (const method of [
"construct", "construct",
"defineProperty", "defineProperty",
"deleteProperty", "deleteProperty",
"get",
"getOwnPropertyDescriptor", "getOwnPropertyDescriptor",
"getPrototypeOf", "getPrototypeOf",
"has", "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 * On first property access, the lazy is evaluated
* @param factory lazy factory * @param factory lazy factory
* @param attempts how many times to try to evaluate the lazy before giving up * @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} * Note that the example below exists already as an api, see {@link findByPropsLazy}
* @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah); * @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah);
*/ */
export function proxyLazy<T>(factory: () => T, attempts = 5): T { export function proxyLazy<T>(factory: () => T, attempts = 5, isChild = false): T {
let isSameTick = true;
if (!isChild)
setTimeout(() => isSameTick = false, 0);
let tries = 0; let tries = 0;
const proxyDummy = Object.assign(function () { }, { const proxyDummy = Object.assign(function () { }, {
[kCACHE]: void 0 as T | undefined, [kCACHE]: void 0 as T | undefined,
@ -100,5 +103,21 @@ export function proxyLazy<T>(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;
} }

29
src/utils/lazyReact.tsx Normal file
View file

@ -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<T extends object = any>(factory: () => React.ComponentType<T>, attempts = 5) {
const get = makeLazy(factory, attempts);
const LazyComponent = (props: T) => {
const Component = get() ?? NoopComponent;
return <Component {...props} />;
};
LazyComponent.$$get = get;
return LazyComponent as ComponentType<T>;
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { filters, findByProps, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; import { findByProps, findByPropsLazy } from "@webpack";
import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react"; import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react";
import { LazyComponent } from "./react"; import { LazyComponent } from "./react";
@ -49,13 +49,7 @@ export interface ModalOptions {
type RenderFunction = (props: ModalProps) => ReactNode; type RenderFunction = (props: ModalProps) => ReactNode;
export const Modals = mapMangledModuleLazy(".closeWithCircleBackground", { export const Modals = findByPropsLazy("ModalRoot", "ModalCloseButton") as {
ModalRoot: filters.byCode(".root"),
ModalHeader: filters.byCode(".header"),
ModalContent: filters.byCode(".content"),
ModalFooter: filters.byCode(".footerSeparator"),
ModalCloseButton: filters.byCode(".closeWithCircleBackground"),
}) as {
ModalRoot: ComponentType<PropsWithChildren<{ ModalRoot: ComponentType<PropsWithChildren<{
transitionState: ModalTransitionState; transitionState: ModalTransitionState;
size?: ModalSize; size?: ModalSize;

View file

@ -18,9 +18,10 @@
import { React, useEffect, useMemo, useReducer, useState } from "@webpack/common"; import { React, useEffect, useMemo, useReducer, useState } from "@webpack/common";
import { makeLazy } from "./lazy";
import { checkIntersecting } from "./misc"; import { checkIntersecting } from "./misc";
export * from "./lazyReact";
export const NoopComponent = () => null; export const NoopComponent = () => null;
/** /**
@ -77,7 +78,6 @@ interface AwaiterOpts<T> {
* @param fallbackValue The fallback value that will be used until the promise resolved * @param fallbackValue The fallback value that will be used until the promise resolved
* @returns [value, error, isPending] * @returns [value, error, isPending]
*/ */
export function useAwaiter<T>(factory: () => Promise<T>): AwaiterRes<T | null>; export function useAwaiter<T>(factory: () => Promise<T>): AwaiterRes<T | null>;
export function useAwaiter<T>(factory: () => Promise<T>, providedOpts: AwaiterOpts<T>): AwaiterRes<T>; export function useAwaiter<T>(factory: () => Promise<T>, providedOpts: AwaiterOpts<T>): AwaiterRes<T>;
export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterOpts<T | null>): AwaiterRes<T | null> { export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterOpts<T | null>): AwaiterRes<T | null> {
@ -113,31 +113,16 @@ export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterO
return [state.value, state.error, state.pending]; return [state.value, state.error, state.pending];
} }
/** /**
* Returns a function that can be used to force rerender react components * Returns a function that can be used to force rerender react components
*/ */
export function useForceUpdater(): () => void; export function useForceUpdater(): () => void;
export function useForceUpdater(withDep: true): [unknown, () => void]; export function useForceUpdater(withDep: true): [unknown, () => void];
export function useForceUpdater(withDep?: true) { export function useForceUpdater(withDep?: true) {
const r = useReducer(x => x + 1, 0); const r = useReducer(x => x + 1, 0);
return withDep ? r : r[1]; 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<T extends object = any>(factory: () => React.ComponentType<T>, attempts = 5) {
const get = makeLazy(factory, attempts);
return (props: T) => {
const Component = get() ?? NoopComponent;
return <Component {...props} />;
};
}
interface TimerOpts { interface TimerOpts {
interval?: number; interval?: number;

View file

@ -41,6 +41,8 @@ export interface Patch {
all?: boolean; all?: boolean;
/** Do not warn if this patch did no changes */ /** Do not warn if this patch did no changes */
noWarn?: boolean; 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; predicate?(): boolean;
} }
@ -80,6 +82,11 @@ export interface PluginDef {
* Whether this plugin should be enabled by default, but can be disabled * Whether this plugin should be enabled by default, but can be disabled
*/ */
enabledByDefault?: boolean; 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. * Optionally provide settings that the user can configure in the Plugins tab of settings.
* @deprecated Use `settings` instead * @deprecated Use `settings` instead
@ -117,6 +124,15 @@ export interface PluginDef {
tags?: string[]; 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 { export const enum OptionType {
STRING, STRING,
NUMBER, NUMBER,

View file

@ -19,9 +19,11 @@
import { LazyComponent } from "@utils/react"; import { LazyComponent } from "@utils/react";
// eslint-disable-next-line path-alias/no-relative // eslint-disable-next-line path-alias/no-relative
import { FilterFn, filters, waitFor } from "../webpack"; import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack";
export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]): T { export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]): T {
if (IS_DEV) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]);
let myValue: T = function () { let myValue: T = function () {
throw new Error(`Vencord could not find the ${name} Component`); throw new Error(`Vencord could not find the ${name} Component`);
} as any; } as any;
@ -30,11 +32,13 @@ export function waitForComponent<T extends React.ComponentType<any> = React.Comp
waitFor(filter, (v: any) => { waitFor(filter, (v: any) => {
myValue = v; myValue = v;
Object.assign(lazyComponent, v); Object.assign(lazyComponent, v);
}); }, { isIndirect: true });
return lazyComponent; return lazyComponent;
} }
export function waitForStore(name: string, cb: (v: any) => void) { 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 });
} }

View file

@ -17,16 +17,12 @@
*/ */
// eslint-disable-next-line path-alias/no-relative // 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"; import type * as t from "./types/menu";
export let Menu = {} as t.Menu; export let Menu = {} as t.Menu;
waitFor(["MenuItem", "MenuSliderControl"], m => Menu = m); waitFor(["MenuItem", "MenuSliderControl"], m => Menu = m);
export const ContextMenu: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN"', { export const ContextMenuApi: t.ContextMenuApi = findByPropsLazy("closeContextMenu", "openContextMenu");
open: filters.byCode("stopPropagation"),
openLazy: m => m.toString().length < 50,
close: filters.byCode("CONTEXT_MENU_CLOSE")
});

View file

@ -16,11 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { proxyLazy } from "@utils/lazy";
import type * as Stores from "discord-types/stores"; import type * as Stores from "discord-types/stores";
// eslint-disable-next-line path-alias/no-relative // eslint-disable-next-line path-alias/no-relative
import { filters, findByProps, findByPropsLazy, mapMangledModuleLazy } from "../webpack"; import { findByPropsLazy } from "../webpack";
import { waitForStore } from "./internal"; import { waitForStore } from "./internal";
import * as t from "./types/stores"; import * as t from "./types/stores";
@ -63,10 +62,6 @@ export let EmojiStore: t.EmojiStore;
export let WindowStore: t.WindowStore; export let WindowStore: t.WindowStore;
export let DraftStore: t.DraftStore; 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 * 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 * 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); * @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
*/ */
export const useStateFromStores: <T>( export const { useStateFromStores }: {
useStateFromStores: <T>(
stores: t.FluxStore[], stores: t.FluxStore[],
mapper: () => T, mapper: () => T,
idk?: any, idk?: any,
isEqual?: (old: T, newer: T) => boolean isEqual?: (old: T, newer: T) => boolean
) => T ) => T;
= proxyLazy(() => findByProps("useStateFromStores").useStateFromStores); }
= findByPropsLazy("useStateFromStores");
waitForStore("DraftStore", s => DraftStore = s); waitForStore("DraftStore", s => DraftStore = s);
waitForStore("UserStore", s => UserStore = s); waitForStore("UserStore", s => UserStore = s);

View file

@ -75,14 +75,14 @@ export interface Menu {
} }
export interface ContextMenuApi { export interface ContextMenuApi {
close(): void; closeContextMenu(): void;
open( openContextMenu(
event: UIEvent, event: UIEvent,
render?: Menu["Menu"], render?: Menu["Menu"],
options?: { enableSpellCheck?: boolean; }, options?: { enableSpellCheck?: boolean; },
renderLazy?: () => Promise<Menu["Menu"]> renderLazy?: () => Promise<Menu["Menu"]>
): void; ): void;
openLazy( openContextMenuLazy(
event: UIEvent, event: UIEvent,
renderLazy?: () => Promise<Menu["Menu"]>, renderLazy?: () => Promise<Menu["Menu"]>,
options?: { enableSpellCheck?: boolean; } options?: { enableSpellCheck?: boolean; }

View file

@ -159,5 +159,26 @@ export interface i18n {
loadPromise: Promise<void>; loadPromise: Promise<void>;
Messages: Record<i18nMessages, string>; Messages: Record<i18nMessages, any>;
}
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;
} }

View file

@ -16,14 +16,23 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { proxyLazy } from "@utils/lazy";
import type { Channel, User } from "discord-types/general"; import type { Channel, User } from "discord-types/general";
// eslint-disable-next-line path-alias/no-relative // 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"; import type * as t from "./types/utils";
export let FluxDispatcher: t.FluxDispatcher; 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; export let ComponentDispatch;
waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.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 RestAPI: t.RestAPI = findByPropsLazy("getAPIBaseURL", "get");
export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear"); 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"); export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep");
@ -41,7 +50,9 @@ export let SnowflakeUtils: t.SnowflakeUtils;
waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m); waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m);
export let Parser: t.Parser; export let Parser: t.Parser;
waitFor("parseTopic", m => Parser = m);
export let Alerts: t.Alerts; export let Alerts: t.Alerts;
waitFor(["show", "close"], m => Alerts = m);
const ToastType = { const ToastType = {
MESSAGE: 0, 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 * 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<string[]>; fetchAssetIds: (applicationId: string, e: string[]) => Promise<string[]>;
}; };
export const Clipboard = mapMangledModuleLazy('document.queryCommandEnabled("copy")||document.queryCommandSupported("copy")', { export const Clipboard: t.Clipboard = findByPropsLazy("SUPPORTS_COPY", "copy");
copy: filters.byCode(".copy("),
SUPPORTS_COPY: x => typeof x === "boolean",
});
export const NavigationRouter = mapMangledModuleLazy("transitionToGuild - ", { export const NavigationRouter: t.NavigationRouter = findByPropsLazy("transitionTo", "replaceWith", "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 let SettingsRouter: any; export let SettingsRouter: any;
waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m); 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 };

View file

@ -119,12 +119,9 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
// Additionally, `[actual newline]` is one less char than "\n", so if Discord // Additionally, `[actual newline]` is one less char than "\n", so if Discord
// ever targets newer browsers, the minifier could potentially use this trick and // ever targets newer browsers, the minifier could potentially use this trick and
// cause issues. // cause issues.
let code: string = mod.toString().replaceAll("\n", ""); //
// a very small minority of modules use function() instead of arrow functions, // 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
// but, unnamed toplevel functions aren't valid. However 0, function() makes it a statement let code: string = "0," + mod.toString().replaceAll("\n", "");
if (code.startsWith("function(")) {
code = "0," + code;
}
const originalMod = mod; const originalMod = mod;
const patchedBy = new Set(); const patchedBy = new Set();
@ -170,19 +167,10 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
if (filter(exports)) { if (filter(exports)) {
subscriptions.delete(filter); subscriptions.delete(filter);
callback(exports, numberId); callback(exports, numberId);
} else if (typeof exports === "object") { } else if (exports.default && filter(exports.default)) {
if (exports.default && filter(exports.default)) {
subscriptions.delete(filter); subscriptions.delete(filter);
callback(exports.default, numberId); 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) { } catch (err) {
logger.error("Error while firing callback for webpack chunk", err); logger.error("Error while firing callback for webpack chunk", err);
} }
@ -191,10 +179,8 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
// for some reason throws some error on which calling .toString() leads to infinite recursion // for some reason throws some error on which calling .toString() leads to infinite recursion
// when you force load all chunks??? // when you force load all chunks???
try {
factory.toString = () => mod.toString(); factory.toString = () => mod.toString();
factory.original = originalMod; factory.original = originalMod;
} catch { }
for (let i = 0; i < patches.length; i++) { for (let i = 0; i < patches.length; i++) {
const patch = patches[i]; const patch = patches[i];
@ -204,6 +190,9 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
if (code.includes(patch.find)) { if (code.includes(patch.find)) {
patchedBy.add(patch.plugin); patchedBy.add(patch.plugin);
const previousMod = mod;
const previousCode = code;
// we change all patch.replacement to array in plugins/index // we change all patch.replacement to array in plugins/index
for (const replacement of patch.replacement as PatchReplacement[]) { for (const replacement of patch.replacement as PatchReplacement[]) {
if (replacement.predicate && !replacement.predicate()) continue; if (replacement.predicate && !replacement.predicate()) continue;
@ -214,12 +203,21 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
try { try {
const newCode = executePatch(replacement.match, replacement.replace as string); const newCode = executePatch(replacement.match, replacement.replace as string);
if (newCode === code && !patch.noWarn) { if (newCode === code) {
(window.explosivePlugins ??= new Set<string>()).add(patch.plugin); if (!patch.noWarn) {
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
if (IS_DEV) { if (IS_DEV) {
logger.debug("Function Source:\n", code); 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 { } else {
code = newCode; code = newCode;
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
@ -259,9 +257,17 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff"); const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements); logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
} }
patchedBy.delete(patch.plugin);
if (patch.group) {
logger.warn(`Undoing patch ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
code = previousCode;
mod = previousMod;
break;
}
code = lastCode; code = lastCode;
mod = lastMod; mod = lastMod;
patchedBy.delete(patch.plugin);
} }
} }

View file

@ -17,6 +17,7 @@
*/ */
import { proxyLazy } from "@utils/lazy"; import { proxyLazy } from "@utils/lazy";
import { LazyComponent } from "@utils/lazyReact";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import type { WebpackInstance } from "discord-types/other"; import type { WebpackInstance } from "discord-types/other";
@ -51,7 +52,18 @@ export const filters = {
return true; return true;
}, },
byStoreName: (name: string): FilterFn => m => byStoreName: (name: string): FilterFn => 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<FilterFn, CallbackFn>(); export const subscriptions = new Map<FilterFn, CallbackFn>();
@ -67,44 +79,6 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
if (!wreq) return false; if (!wreq) return false;
cache = wreq.c; 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; return true;
} }
@ -140,20 +114,10 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn
return isWaitFor ? [mod.exports, Number(key)] : mod.exports; return isWaitFor ? [mod.exports, Number(key)] : mod.exports;
} }
if (typeof mod.exports !== "object") continue;
if (mod.exports.default && filter(mod.exports.default)) { if (mod.exports.default && filter(mod.exports.default)) {
const found = mod.exports.default; const found = mod.exports.default;
return isWaitFor ? [found, Number(key)] : found; 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) { if (!isIndirect) {
@ -163,13 +127,6 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn
return isWaitFor ? [null, null] : null; return isWaitFor ? [null, null] : null;
}); });
/**
* find but lazy
*/
export function findLazy(filter: FilterFn) {
return proxyLazy(() => find(filter));
}
export function findAll(filter: FilterFn) { export function findAll(filter: FilterFn) {
if (typeof filter !== "function") if (typeof filter !== "function")
throw new Error("Invalid filter. Expected a function got " + typeof filter); throw new Error("Invalid filter. Expected a function got " + typeof filter);
@ -181,15 +138,9 @@ export function findAll(filter: FilterFn) {
if (filter(mod.exports)) if (filter(mod.exports))
ret.push(mod.exports); ret.push(mod.exports);
else if (typeof mod.exports !== "object")
continue;
if (mod.exports.default && filter(mod.exports.default)) if (mod.exports.default && filter(mod.exports.default))
ret.push(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; return ret;
@ -239,26 +190,12 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns
break; break;
} }
if (typeof mod.exports !== "object")
continue;
if (mod.exports.default && filter(mod.exports.default)) { if (mod.exports.default && filter(mod.exports.default)) {
results[j] = mod.exports.default; results[j] = mod.exports.default;
filters[j] = undefined; filters[j] = undefined;
if (++found === length) break outer; if (++found === length) break outer;
break; 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; 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) * This is just a wrapper around {@link proxyLazy} to make our reporter test for your webpack finds.
* 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
* *
* @example mapMangledModule("headerIdIsManaged:", { * Wraps the result of {@link makeLazy} in a Proxy you can consume as if it wasn't lazy.
* openModal: filters.byCode("headerIdIsManaged:"), * On first property access, the lazy is evaluated
* closeModal: filters.byCode("key==") * @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<S extends string>(code: string, mappers: Record<S, FilterFn>): Record<S, any> { export function proxyLazyWebpack<T = any>(factory: () => any, attempts?: number) {
const exports = {} as Record<S, any>; if (IS_DEV) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]);
const id = findModuleId(code); return proxyLazy<T>(factory, attempts);
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;
});
/** /**
* 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<S extends string>(code: string, mappers: Record<S, FilterFn>): Record<S, any> { export function LazyComponentWebpack<T extends object = any>(factory: () => any, attempts?: number) {
return proxyLazy(() => mapMangledModule(code, mappers)); if (IS_DEV) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]);
return LazyComponent<T>(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 * findByProps but lazy
*/ */
export function findByPropsLazy(...props: string[]) { export function findByPropsLazy(...props: string[]) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findByProps", props]);
return proxyLazy(() => findByProps(...props)); return proxyLazy(() => findByProps(...props));
} }
@ -372,6 +313,8 @@ export function findByCode(...code: string[]) {
* findByCode but lazy * findByCode but lazy
*/ */
export function findByCodeLazy(...code: string[]) { export function findByCodeLazy(...code: string[]) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findByCode", code]);
return proxyLazy(() => 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) { export function findStoreLazy(name: string) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findStore", [name]]);
return proxyLazy(() => 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<T extends object = any>(filter: FilterFn) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findComponent", [filter]]);
return LazyComponent<T>(() => find(filter));
}
/**
* Finds the first component that includes all the given code, lazily
*/
export function findComponentByCodeLazy<T extends object = any>(...code: string[]) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findComponentByCode", code]);
return LazyComponent<T>(() => findComponentByCode(...code));
}
/**
* Finds the first component that is exported by the first prop name, lazily
*/
export function findExportedComponentLazy<T extends object = any>(...props: string[]) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findExportedComponent", props]);
return LazyComponent<T>(() => findByProps(...props)?.[props[0]]);
}
/** /**
* Wait for a module that matches the provided filter to be registered, * Wait for a module that matches the provided filter to be registered,
* then call the callback with the module as the first argument * 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") if (typeof filter === "string")
filter = filters.byProps(filter); filter = filters.byProps(filter);
else if (Array.isArray(filter)) else if (Array.isArray(filter))