Compare commits

..

57 commits

Author SHA1 Message Date
beaf164aa8
owo 2023-12-14 16:47:35 +09:00
Nuckyz
2cd82944e3
Move commons from discord utils; Make ThemesTab use invite modal util 2023-12-13 22:01:07 -03:00
Nuckyz
2f1dc2c704
reporter: fix icon 2023-12-13 21:46:51 -03:00
Nuckyz
a5442d87d5
Fix reporter and AlwaysAnimate patch 2023-12-13 21:41:09 -03:00
V
a8b0ce6f03
fix(notrack): murder sentry 2023-12-14 01:29:57 +01:00
Andrew Grant
b32959126e
TypingIndicator: setting to disable for current channel (#2043) 2023-12-13 19:54:09 -03:00
zImPatrick
2cf52d0775
AlwaysAnimate: Add guild banner (#2036) 2023-12-13 20:41:50 +00:00
sappho
40b3ec57ce
FakeNitro: fix non apng gif stickers being sent as images (#2050) 2023-12-13 06:37:31 -03:00
ruukulada
817cb9b60b
GameActivityToggle: Icon cleanup (#2041) 2023-12-12 23:40:13 -03:00
Nuckyz
c86de3299e
fixAll.eslint: true -> "explicit" 2023-12-12 23:29:22 -03:00
Nuckyz
1df0b571af
Fix broken patches 2023-12-12 22:57:32 -03:00
AutumnVN
6d911790e9
oneko: allow oneko in reduced motion (#2018) 2023-12-10 02:05:40 +01:00
Nuckyz
c9f7cf7540
ci: test all branches 2023-12-09 18:34:58 -03:00
Nuckyz
a9568bc055
reporter: fix bad logic 2023-12-09 17:57:25 -03:00
Nuckyz
539e538d87
commandHelpers: use MessageActions import 2023-12-09 17:57:25 -03:00
AutumnVN
799e6e7292
platformIndicators: fix (#2038) 2023-12-09 21:55:13 +01:00
maisy
510bfb8fa3
ReactErrorDecoder: fix using wrong react version's error map (#2040)
Co-authored-by: V <vendicated@riseup.net>
2023-12-09 20:36:58 +00:00
AutumnVN
534ca1e28d
fix: createBotMessage (#2033) 2023-12-07 02:44:14 +01:00
Vendicated
d629281e72
bump to v1.6.5 2023-12-07 00:35:05 +01:00
Damien Erambert
34cbb22efe
feat: add dropdown to choose vibrancy value on macOS (#1941)
Co-authored-by: V <vendicated@riseup.net>
2023-12-07 00:30:41 +01:00
Ajay Ramachandran
6ee50d30f6
fix(dearrow): remove > from DeArrow titles (#1999) 2023-12-07 00:26:08 +01:00
Han Seung Min - 한승민
fd9c675942
fix: patch helper overflow (#2007) 2023-12-07 00:25:13 +01:00
AutumnVN
920252956f
shikiCodeBlocks: support file preview (#1977) 2023-12-06 22:42:40 +00:00
Nickyux
e4942397dc
MessageClickActions: Ignore Ephemeral & Deleted Messages (#1972)
Co-authored-by: V <vendicated@riseup.net>
2023-12-06 22:18:03 +00:00
philipbry
9faa1331da
new plugin: notificationVolume (#1992)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: V <vendicated@riseup.net>
2023-12-06 22:15:34 +00:00
fres621
613b2dc5f6
Fix QuickMention not working in DMs (#2028)
Co-authored-by: V <vendicated@riseup.net>
2023-12-06 22:54:23 +01:00
nya
9a89f7b3b2
feat(plugin): XSOverlay (#1901)
Co-authored-by: V <vendicated@riseup.net>
2023-12-06 22:31:22 +01:00
AutumnVN
c0b6d8f1c4
devCompanion: add findComponentByCode (#2026) 2023-12-06 22:27:06 +01:00
Carter
3453d0c97c
feat(clearUrls): moar twitter rules (#2029) 2023-12-06 22:25:53 +01:00
Syncx
cf7028331c
Native Folder Support (#2031) 2023-12-06 22:25:29 +01:00
Nuckyz
08036f7af2
convert non lazy finds to test with reporter 2023-12-06 01:37:42 -03:00
Nuckyz
9dd00fb766
Fix SuperReactionTweaks patch 2023-12-01 23:06:18 -03:00
Nuckyz
80016180b6
FixImagesQuality: no longer make gifs play when autoplay is off 2023-12-01 16:39:15 -03:00
megumin
3e7d946296
fix(SpotifyControls): Requests double-sending when using Spotify Connect (#2023) 2023-11-30 16:32:48 -03:00
V
fccdd3dc08
migrate to new badge api
we used to store badges on the discord cdn.
since discord is now making
it harder to use their cdn for such purposes (due to expiring links), we
are forced to stop using it
thus, badges are now stored on our server, accessible via
https://badges.vencord.dev. The full list of badges is now at https://badges.vencord.dev/badges.json
2023-11-30 17:28:53 +01:00
Nuckyz
8e1546be00
Include ignored Discord errors in summary 2023-11-30 02:38:12 -03:00
Nuckyz
66dbe7ef07
Fix reporter testing for extractAndLoadChunks 2023-11-30 02:26:18 -03:00
Jack
b47a5f569e
feat: Add Decor plugin (#910) 2023-11-30 02:10:50 -03:00
Nuckyz
8ef1882d43
use findBulk on CrashHandler and cleaups 2023-11-30 00:43:23 -03:00
Nuckyz
9945219de7
openInviteModal utility
Co-authored-by: AutumnVN <autumnvnchino@gmail.com>
2023-11-29 23:15:19 -03:00
Nuckyz
091d29bf5e
CrashHandler: attempt to prevent more crashes 2023-11-29 16:14:05 -03:00
Nuckyz
9b6308a835
Fix a console shortcut and suppressExperimentalWarnings on more scripts 2023-11-28 22:12:00 -03:00
Nuckyz
597a74ff6c
ClientTheme: make color picker finder more specific 2023-11-28 17:05:11 -03:00
V
f814eeb74c
VoiceMessages: fix preview being blank 2023-11-28 16:33:02 -03:00
Nuckyz
1619ee404a
Utility function for loading Discord chunks (#2017) 2023-11-28 16:33:02 -03:00
Nuckyz
1b179f3c6d
Simplify some components finds; Make undo of patch groups more clear 2023-11-28 16:33:00 -03:00
Korbo
6573c4757c
fix: strikethrough in SilentMessageToggle's disabled SVG (#2004) 2023-11-28 18:32:29 +00:00
V
ec16fd8741
fix ci 2023-11-25 02:55:59 +01:00
V
6bbf562ab6
bump to v1.6.4 2023-11-25 02:51:58 +01:00
V
604cf00211
reporter: fix markdown output 2023-11-25 02:51:19 +01:00
V
7c3b247d84
fix WebContextMenus 2023-11-25 02:43:29 +01:00
V
68fca78541
reporter: test dev instead of main 2023-11-25 02:02:50 +01:00
V
598ffe6368
reporter: remove sleeps 2023-11-25 01:52:11 +01:00
V
534565db25
Add webpack find testing (#2016)
Co-authored-by: V <vendicated@riseup.net>
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2023-11-25 01:32:21 +01:00
Nuckyz
3e8e106be7
Fix broken patches 2023-11-24 16:49:19 -03:00
Nuckyz
fdddfdb05b
feat(plugins): FixImagesQuality 2023-11-24 16:32:30 -03:00
cat
e14fba28a5
ClearURLS for x.com links (#2005) 2023-11-23 14:31:25 -03:00
87 changed files with 2634 additions and 289 deletions

View file

@ -12,6 +12,12 @@ jobs:
steps:
- uses: actions/checkout@v3
if: ${{ github.event_name == 'schedule' }}
with:
ref: dev
- uses: actions/checkout@v3
if: ${{ github.event_name == 'workflow_dispatch' }}
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
@ -29,7 +35,7 @@ jobs:
sudo apt-get install -y chromium-browser
- name: Build web
run: pnpm buildWeb --standalone
run: pnpm buildWeb --standalone --dev
- name: Create Report
timeout-minutes: 10

View file

@ -1,11 +1,10 @@
name: test
on:
push:
branches:
- main
pull_request:
branches:
- main
- dev
jobs:
test:
runs-on: ubuntu-latest

View file

@ -1,7 +1,7 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.6.3",
"version": "1.6.5",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -17,7 +17,7 @@
"doc": "docs"
},
"scripts": {
"build": "node scripts/build/build.mjs",
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"generatePluginJson": "tsx scripts/generatePluginList.ts",
"inject": "node scripts/runInstaller.mjs",
@ -28,7 +28,7 @@
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit",
"uninject": "node scripts/runInstaller.mjs",
"watch": "node scripts/build/build.mjs --watch"
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch"
},
"dependencies": {
"@sapphi-red/web-noise-suppressor": "0.3.3",
@ -68,7 +68,8 @@
"tsx": "^3.12.7",
"type-fest": "^3.9.0",
"typescript": "^5.0.4",
"zip-local": "^0.3.5"
"zip-local": "^0.3.5",
"zustand": "^3.7.2"
},
"packageManager": "pnpm@8.10.2",
"pnpm": {

View file

@ -1,9 +1,5 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
eslint-plugin-path-alias@1.0.0:
hash: m6sma4g6bh67km3q6igf6uxaja
@ -123,6 +119,9 @@ devDependencies:
zip-local:
specifier: ^0.3.5
version: 0.3.5
zustand:
specifier: ^3.7.2
version: 3.7.2
packages:
@ -3450,8 +3449,22 @@ packages:
q: 1.5.1
dev: true
/zustand@3.7.2:
resolution: {integrity: sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==}
engines: {node: '>=12.7.0'}
peerDependencies:
react: '>=16.8'
peerDependenciesMeta:
react:
optional: true
dev: true
github.com/mattdesl/gifenc/64842fca317b112a8590f8fef2bf3825da8f6fe3:
resolution: {tarball: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3}
name: gifenc
version: 1.0.3
dev: false
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

View file

@ -21,11 +21,11 @@ import esbuild from "esbuild";
import { readdir } from "fs/promises";
import { join } from "path";
import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isDev, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
const defines = {
IS_STANDALONE: isStandalone,
IS_DEV: JSON.stringify(watch),
IS_DEV: JSON.stringify(isDev),
IS_UPDATER_DISABLED: updaterDisabled,
IS_WEB: false,
IS_EXTENSION: false,
@ -76,7 +76,11 @@ const globNativesPlugin = {
if (!await existsAsync(dirPath)) continue;
const plugins = await readdir(dirPath);
for (const p of plugins) {
if (!await existsAsync(join(dirPath, p, "native.ts"))) continue;
const nativePath = join(dirPath, p, "native.ts");
const indexNativePath = join(dirPath, p, "native/index.ts");
if (!(await existsAsync(nativePath)) && !(await existsAsync(indexNativePath)))
continue;
const nameParts = p.split(".");
const namePartsWithoutTarget = nameParts.length === 1 ? nameParts : nameParts.slice(0, -1);

View file

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

View file

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

View file

@ -34,7 +34,7 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
const CANARY = process.env.USE_CANARY === "true";
const browser = await pup.launch({
headless: true,
headless: "new",
executablePath: process.env.CHROMIUM_BIN
});
@ -58,14 +58,16 @@ const report = {
plugin: string;
error: string;
}[],
otherErrors: [] as string[]
otherErrors: [] as string[],
badWebpackFinds: [] as string[]
};
const IGNORED_DISCORD_ERRORS = [
"KeybindStore: Looking for callback action",
"Unable to process domain list delta: Client revision number is null",
"Downloading the full bad domains file",
/\[GatewaySocket\].{0,110}Cannot access '/
/\[GatewaySocket\].{0,110}Cannot access '/,
"search for 'name' in undefined"
] as Array<string | RegExp>;
function toCodeBlock(s: string) {
@ -74,7 +76,10 @@ function toCodeBlock(s: string) {
}
async function printReport() {
console.log();
console.log("# Vencord Report" + (CANARY ? " (Canary)" : ""));
console.log();
console.log("## Bad Patches");
@ -87,21 +92,43 @@ async function printReport() {
console.log();
console.log("## Bad Webpack Finds");
report.badWebpackFinds.forEach(p => console.log("- " + p));
console.log();
console.log("## Bad Starts");
report.badStarts.forEach(p => {
console.log(`- ${p.plugin}`);
console.log(` - Error: ${toCodeBlock(p.error)}`);
});
report.otherErrors = report.otherErrors.filter(e => !IGNORED_DISCORD_ERRORS.some(regex => e.match(regex)));
console.log();
const ignoredErrors = [] as string[];
report.otherErrors = report.otherErrors.filter(e => {
if (IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))) {
ignoredErrors.push(e);
return false;
}
return true;
});
console.log("## Discord Errors");
report.otherErrors.forEach(e => {
console.log(`- ${toCodeBlock(e)}`);
});
console.log();
console.log("## Ignored Discord Errors");
ignoredErrors.forEach(e => {
console.log(`- ${toCodeBlock(e)}`);
});
console.log();
if (process.env.DISCORD_WEBHOOK) {
// this code was written almost entirely by Copilot xD
await fetch(process.env.DISCORD_WEBHOOK, {
method: "POST",
headers: {
@ -110,7 +137,7 @@ async function printReport() {
body: JSON.stringify({
description: "Here's the latest Vencord Report!",
username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/f0204a918c6c9c9a43195997e97d8adf.webp",
avatar_url: "https://cdn.discordapp.com/avatars/1017176847865352332/c312b6b44179ae6817de7e4b09e9c6af.webp?size=512",
embeds: [
{
title: "Bad Patches",
@ -125,6 +152,11 @@ async function printReport() {
}).join("\n\n") || "None",
color: report.badPatches.length ? 0xff0000 : 0x00ff00
},
{
title: "Bad Webpack Finds",
description: report.badWebpackFinds.map(toCodeBlock).join("\n") || "None",
color: report.badWebpackFinds.length ? 0xff0000 : 0x00ff00
},
{
title: "Bad Starts",
description: report.badStarts.map(p => {
@ -153,29 +185,38 @@ async function printReport() {
page.on("console", async e => {
const level = e.type();
const args = e.args();
const rawArgs = e.args();
const firstArg = (await args[0]?.jsonValue());
if (firstArg === "PUPPETEER_TEST_DONE_SIGNAL") {
const firstArg = await rawArgs[0]?.jsonValue();
if (firstArg === "[PUPPETEER_TEST_DONE_SIGNAL]") {
await browser.close();
await printReport();
process.exit();
}
const isVencord = (await args[0]?.jsonValue()) === "[Vencord]";
const isDebug = (await args[0]?.jsonValue()) === "[PUP_DEBUG]";
const isVencord = firstArg === "[Vencord]";
const isDebug = firstArg === "[PUP_DEBUG]";
const isWebpackFindFail = firstArg === "[PUP_WEBPACK_FIND_FAIL]";
if (isWebpackFindFail) {
process.exitCode = 1;
report.badWebpackFinds.push(await rawArgs[1].jsonValue() as string);
}
if (isVencord) {
// make ci fail
process.exitCode = 1;
const args = await Promise.all(e.args().map(a => a.jsonValue()));
const jsonArgs = await Promise.all(args.map(a => a.jsonValue()));
const [, tag, message] = jsonArgs;
const cause = await maybeGetError(args[3]);
const [, tag, message] = args as Array<string>;
const cause = await maybeGetError(e.args()[3]);
switch (tag) {
case "WebpackInterceptor:":
const [, plugin, type, id, regex] = (message as string).match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
if (!patchFailMatch) break;
process.exitCode = 1;
const [, plugin, type, id, regex] = patchFailMatch;
report.badPatches.push({
plugin,
type,
@ -183,16 +224,25 @@ page.on("console", async e => {
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
error: cause
});
break;
case "PluginManager:":
const [, name] = (message as string).match(/Failed to start (.+)/)!;
const failedToStartMatch = message.match(/Failed to start (.+)/);
if (!failedToStartMatch) break;
process.exitCode = 1;
const [, name] = failedToStartMatch;
report.badStarts.push({
plugin: name,
error: cause
});
break;
}
} else if (isDebug) {
}
if (isDebug) {
console.error(e.text());
} else if (level === "error") {
const text = await Promise.all(
@ -206,8 +256,8 @@ page.on("console", async e => {
).then(a => a.join(" ").trim());
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of")) {
console.error("Got unexpected error", text);
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
console.error("[Unexpected Error]", text);
report.otherErrors.push(text);
}
}
@ -219,17 +269,16 @@ page.on("pageerror", e => console.error("[Page Error]", e));
await page.setBypassCSP(true);
function runTime(token: string) {
console.error("[PUP_DEBUG]", "Starting test...");
console.log("[PUP_DEBUG]", "Starting test...");
try {
// spoof languages to not be suspicious
// Spoof languages to not be suspicious
Object.defineProperty(navigator, "languages", {
get: function () {
return ["en-US", "en"];
},
});
// Monkey patch Logger to not log with custom css
// @ts-ignore
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
@ -237,7 +286,7 @@ function runTime(token: string) {
console[level]("[Vencord]", this.name + ":", ...args);
};
// force enable all plugins and patches
// Force enable all plugins and patches
Vencord.Plugins.patches.length = 0;
Object.values(Vencord.Plugins.plugins).forEach(p => {
// Needs native server to run
@ -247,8 +296,15 @@ function runTime(token: string) {
p.patches?.forEach(patch => {
patch.plugin = p.name;
delete patch.predicate;
delete patch.group;
if (!Array.isArray(patch.replacement))
patch.replacement = [patch.replacement];
patch.replacement.forEach(r => {
delete r.predicate;
});
Vencord.Plugins.patches.push(patch);
});
});
@ -256,41 +312,145 @@ function runTime(token: string) {
Vencord.Webpack.waitFor(
"loginToken",
m => {
console.error("[PUP_DEBUG]", "Logging in with token...");
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}
);
// force load all chunks
// Force load all chunks
Vencord.Webpack.onceReady.then(() => setTimeout(async () => {
console.error("[PUP_DEBUG]", "Webpack is ready!");
console.log("[PUP_DEBUG]", "Webpack is ready!");
const { wreq } = Vencord.Webpack;
console.error("[PUP_DEBUG]", "Loading all chunks...");
const ids = Function("return" + wreq.u.toString().match(/(?<=\()\{.+?\}/s)![0])();
for (const id in ids) {
console.log("[PUP_DEBUG]", "Loading all chunks...");
let chunks = null as Record<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 = new Set<string>();
const validChunks = new Set<string>();
const invalidChunks = new Set<string>();
if (!chunks) throw new Error("Failed to get chunks");
for (const entryPoint in chunks) {
const chunkIds = chunks[entryPoint];
let invalidEntryPoint = false;
for (const id of chunkIds) {
if (!wreq.u(id)) continue;
const isWasm = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
if (!isWasm)
await wreq.e(id as any);
await new Promise(r => setTimeout(r, 150));
if (isWasm) {
invalidChunks.add(id);
invalidEntryPoint = true;
continue;
}
console.error("[PUP_DEBUG]", "Finished loading chunks!");
validChunks.add(id);
}
if (!invalidEntryPoint)
validChunksEntryPoints.add(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.has(id) || invalidChunks.has(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) {
if (!patch.all) {
new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
}
}
setTimeout(() => console.log("PUPPETEER_TEST_DONE_SIGNAL"), 1000);
for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) {
let method = searchType;
if (searchType === "findComponent") method = "find";
if (searchType === "findExportedComponent") method = "findByProps";
if (searchType === "waitFor" || searchType === "waitForComponent" || searchType === "waitForStore") {
if (typeof args[0] === "string") method = "findByProps";
else method = "find";
}
try {
let result: any;
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
const [factory] = args;
result = factory();
} else if (method === "extractAndLoadChunks") {
const [code, matcher] = args;
const module = Vencord.Webpack.findModuleFactory(...code);
if (module) result = module.toString().match(Vencord.Util.canonicalizeMatch(matcher));
} else {
// @ts-ignore
result = Vencord.Webpack[method](...args);
}
if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == 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 if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
}
}
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
}, 1000));
} catch (e) {
console.error("[PUP_DEBUG]", "A fatal error occurred");
console.error("[PUP_DEBUG]", e);
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
process.exit(1);
}
}

View file

@ -18,14 +18,13 @@
import { mergeDefaults } from "@utils/misc";
import { findByPropsLazy } from "@webpack";
import { SnowflakeUtils } from "@webpack/common";
import { MessageActions, SnowflakeUtils } from "@webpack/common";
import { Message } from "discord-types/general";
import type { PartialDeep } from "type-fest";
import { Argument } from "./types";
const MessageCreator = findByPropsLazy("createBotMessage");
const MessageSender = findByPropsLazy("receiveMessage");
export function generateId() {
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
@ -40,7 +39,7 @@ export function generateId() {
export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
const botMessage = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] });
MessageSender.receiveMessage(channelId, mergeDefaults(message, botMessage));
MessageActions.receiveMessage(channelId, mergeDefaults(message, botMessage));
return message as Message;
}

View file

@ -38,7 +38,21 @@ export interface Settings {
frameless: boolean;
transparent: boolean;
winCtrlQ: boolean;
macosTranslucency: boolean;
macosVibrancyStyle:
| "content"
| "fullscreen-ui"
| "header"
| "hud"
| "menu"
| "popover"
| "selection"
| "sidebar"
| "titlebar"
| "tooltip"
| "under-page"
| "window"
| undefined;
macosTranslucency: boolean | undefined;
disableMinSize: boolean;
winNativeTitleBar: boolean;
plugins: {
@ -74,7 +88,9 @@ const DefaultSettings: Settings = {
frameless: false,
transparent: false,
winCtrlQ: false,
macosTranslucency: false,
// Replaced by macosVibrancyStyle
macosTranslucency: undefined,
macosVibrancyStyle: undefined,
disableMinSize: false,
winNativeTitleBar: false,
plugins: {},

View file

@ -255,3 +255,38 @@ export function DeleteIcon(props: IconProps) {
</Icon>
);
}
export function PlusIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-plus-icon")}
viewBox="0 0 18 18"
>
<polygon
fill-rule="nonzero"
fill="currentColor"
points="15 10 10 10 10 15 8 15 8 10 3 10 3 8 8 8 8 3 10 3 10 8 15 8"
/>
</Icon>
);
}
export function NoEntrySignIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-no-entry-sign-icon")}
viewBox="0 0 24 24"
>
<path
d="M0 0h24v24H0z"
fill="none"
/>
<path
fill="currentColor"
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8 0-1.85.63-3.55 1.69-4.9L16.9 18.31C15.55 19.37 13.85 20 12 20zm6.31-3.1L7.1 5.69C8.45 4.63 10.15 4 12 4c4.42 0 8 3.58 8 8 0 1.85-.63 3.55-1.69 4.9z"
/>
</Icon>
);
}

View file

@ -108,7 +108,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
function renderDiff() {
return diff?.map(p => {
const color = p.added ? "lime" : p.removed ? "red" : "grey";
return <div style={{ color, userSelect: "text" }}>{p.value}</div>;
return <div style={{ color, userSelect: "text", wordBreak: "break-all", lineBreak: "anywhere" }}>{p.value}</div>;
});
}

View file

@ -21,12 +21,13 @@ import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findLazy } from "@webpack";
import { Button, Card, FluxDispatcher, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import { UserThemeHeader } from "main/themes";
import type { ComponentType, Ref, SyntheticEvent } from "react";
@ -125,15 +126,7 @@ function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) {
href={`https://discord.gg/${theme.invite}`}
onClick={async e => {
e.preventDefault();
const { invite } = await InviteActions.resolveInvite(theme.invite, "Desktop Modal");
if (!invite) return showToast("Invalid or expired invite");
FluxDispatcher.dispatch({
type: "INVITE_MODAL_OPEN",
invite,
code: theme.invite,
context: "APP"
});
theme.invite != null && openInviteModal(theme.invite).catch(() => showToast("Invalid or expired invite"));
}}
>
Discord Server

View file

@ -48,6 +48,15 @@ function VencordSettings() {
const isWindows = navigator.platform.toLowerCase().startsWith("win");
const isMac = navigator.platform.toLowerCase().startsWith("mac");
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
// One-time migration of the old setting to the new one if necessary.
React.useEffect(() => {
if (settings.macosTranslucency === true && !settings.macosVibrancyStyle) {
settings.macosVibrancyStyle = "sidebar";
settings.macosTranslucency = undefined;
}
}, []);
const Switches: Array<false | {
key: KeysOfType<typeof settings, boolean>;
@ -89,11 +98,6 @@ function VencordSettings() {
title: "Disable minimum window size",
note: "Requires a full restart"
},
IS_DISCORD_DESKTOP && isMac && {
key: "macosTranslucency",
title: "Enable translucent window",
note: "Requires a full restart"
}
];
return (
@ -152,6 +156,71 @@ function VencordSettings() {
</Forms.FormSection>
{needsVibrancySettings && <>
<Forms.FormTitle tag="h5">Window vibrancy style (requires restart)</Forms.FormTitle>
<Select
className={Margins.bottom20}
placeholder="Window vibrancy style"
options={[
// Sorted from most opaque to most transparent
{
label: "No vibrancy", default: !settings.macosTranslucency, value: undefined
},
{
label: "Under Page (window tinting)",
value: "under-page"
},
{
label: "Content",
value: "content"
},
{
label: "Window",
value: "window"
},
{
label: "Selection",
value: "selection"
},
{
label: "Titlebar",
value: "titlebar"
},
{
label: "Header",
value: "header"
},
{
label: "Sidebar (old value for transparent windows)",
value: "sidebar",
default: settings.macosTranslucency
},
{
label: "Tooltip",
value: "tooltip"
},
{
label: "Menu",
value: "menu"
},
{
label: "Popover",
value: "popover"
},
{
label: "Fullscreen UI (transparent but slightly muted)",
value: "fullscreen-ui"
},
{
label: "HUD (Most transparent)",
value: "hud"
},
]}
select={v => settings.macosVibrancyStyle = v}
isSelected={v => settings.macosVibrancyStyle === v}
serialize={identity} />
</>}
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
</SettingsTab>
);

View file

@ -85,9 +85,15 @@ if (!IS_VANILLA) {
options.backgroundColor = "#00000000";
}
if (settings.macosTranslucency && process.platform === "darwin") {
const needsVibrancy = process.platform === "darwin" || (settings.macosVibrancyStyle || settings.macosTranslucency);
if (needsVibrancy) {
options.backgroundColor = "#00000000";
if (settings.macosTranslucency) {
options.vibrancy = "sidebar";
} else if (settings.macosVibrancyStyle) {
options.vibrancy = settings.macosVibrancyStyle;
}
}
process.env.DISCORD_PRELOAD = original;

View file

@ -22,14 +22,13 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Heart } from "@components/Heart";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc";
import { closeModal, Modals, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Forms, Toasts } from "@webpack/common";
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/attachments/1033680203433660458/1092089947126780035/favicon.png";
const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png";
const ContributorBadge: ProfileBadge = {
description: "Vencord Contributor",
@ -45,7 +44,7 @@ const ContributorBadge: ProfileBadge = {
link: "https://github.com/Vendicated/Vencord"
};
let DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "description">[]>;
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
async function loadBadges(noCache = false) {
DonorBadges = {};
@ -54,19 +53,8 @@ async function loadBadges(noCache = false) {
if (noCache)
init.cache = "no-cache";
const badges = await fetch("https://gist.githubusercontent.com/Vendicated/51a3dd775f6920429ec6e9b735ca7f01/raw/badges.csv", init)
.then(r => r.text());
const lines = badges.trim().split("\n");
if (lines.shift() !== "id,tooltip,image") {
new Logger("BadgeAPI").error("Invalid badges.csv file!");
return;
}
for (const line of lines) {
const [id, description, image] = line.split(",");
(DonorBadges[id] ??= []).push({ image, description });
}
DonorBadges = await fetch("https://badges.vencord.dev/badges.json", init)
.then(r => r.json());
}
export default definePlugin({
@ -127,7 +115,8 @@ export default definePlugin({
getDonorBadges(userId: string) {
return DonorBadges[userId]?.map(badge => ({
...badge,
image: badge.badge,
description: badge.tooltip,
position: BadgePosition.START,
props: {
style: {

View file

@ -46,6 +46,14 @@ export default definePlugin({
match: /(?<=\.activityEmoji,.+?animate:)\i/,
replace: "!0"
}
},
{
// Guild Banner
find: ".animatedBannerHoverLayer,onMouseEnter:",
replacement: {
match: /(?<=guildBanner:\i,animate:)\i(?=}\))/,
replace: "!0"
}
}
]
});

View file

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

View file

@ -15,7 +15,7 @@ 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 ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
const colorPresets = [
"#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D",

View file

@ -63,6 +63,7 @@ export default definePlugin({
let fakeRenderWin: WeakRef<Window> | undefined;
const find = newFindWrapper(f => f);
const findByProps = newFindWrapper(filters.byProps);
return {
...Vencord.Webpack.Common,
wp: Vencord.Webpack,
@ -73,13 +74,13 @@ export default definePlugin({
wpexs: (code: string) => extract(Webpack.findModuleId(code)!),
find,
findAll,
findByProps: newFindWrapper(filters.byProps),
findByProps,
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
findByCode: newFindWrapper(filters.byCode),
findAllByCode: (code: string) => findAll(filters.byCode(code)),
findComponentByCode: newFindWrapper(filters.componentByCode),
findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)),
findExportedComponent: (...props: string[]) => find(...props)[props[0]],
findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]],
findStore: newFindWrapper(filters.byStoreName),
PluginsApi: Vencord.Plugins,
plugins: Vencord.Plugins.plugins,

View file

@ -23,12 +23,26 @@ import { Logger } from "@utils/Logger";
import { closeAllModals } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { maybePromptToUpdate } from "@utils/updater";
import { findByPropsLazy } from "@webpack";
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
import { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
import type { ReactElement } from "react";
const CrashHandlerLogger = new Logger("CrashHandler");
const ModalStack = findByPropsLazy("pushLazy", "popAll");
const { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => {
const modules = findBulk(
filters.byProps("pushLazy", "popAll"),
filters.byProps("clearDraft", "saveDraft"),
filters.byProps("DraftType"),
filters.byProps("closeExpressionPicker", "openExpressionPicker"),
);
return {
ModalStack: modules[0],
DraftManager: modules[1],
DraftType: modules[2]?.DraftType,
closeExpressionPicker: modules[3]?.closeExpressionPicker,
};
});
const settings = definePluginSettings({
attemptToPreventCrashes: {
@ -115,13 +129,27 @@ export default definePlugin({
} catch { }
}
try {
const channelId = SelectedChannelStore.getChannelId();
DraftManager.clearDraft(channelId, DraftType.ChannelMessage);
DraftManager.clearDraft(channelId, DraftType.FirstThreadMessage);
} catch (err) {
CrashHandlerLogger.debug("Failed to clear drafts.", err);
}
try {
closeExpressionPicker();
}
catch (err) {
CrashHandlerLogger.debug("Failed to close expression picker.", err);
}
try {
FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" });
} catch (err) {
CrashHandlerLogger.debug("Failed to close open context menu.", err);
}
try {
ModalStack?.popAll();
ModalStack.popAll();
} catch (err) {
CrashHandlerLogger.debug("Failed to close old modals.", err);
}

View file

@ -60,7 +60,7 @@ async function embedDidMount(this: Component<Props>) {
if (hasTitle) {
embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title;
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
}
if (hasThumb) {

View file

@ -0,0 +1,17 @@
# Decor
Custom avatar decorations!
![Custom decorations in chat](https://github.com/Vendicated/Vencord/assets/30497388/b0c4c4c8-8723-42a8-b50f-195ad4e26136)
Create and use your own custom avatar decorations, or pick your favorite from the presets.
You'll be able to see the custom avatar decorations of other users of this plugin, and they'll be able to see your custom avatar decoration.
You can select and manage your custom avatar decorations under the "Profiles" page in settings, or in the plugin settings.
![Custom decorations management](https://github.com/Vendicated/Vencord/assets/30497388/74fe8a9e-a2a2-4b29-bc10-9eaa58208ad4)
Review the [guidelines](https://github.com/decor-discord/.github/blob/main/GUIDELINES.md) before creating your own custom avatar decoration.
Join the [Discord server](https://discord.gg/dXp2SdxDcP) for support and notifications on your decoration's review.

168
src/plugins/decor/index.tsx Normal file
View file

@ -0,0 +1,168 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated, FieryFlames and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./ui/styles.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { closeAllModals } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { FluxDispatcher, Forms, UserStore } from "@webpack/common";
import { CDN_URL, RAW_SKU_ID, SKU_ID } from "./lib/constants";
import { useAuthorizationStore } from "./lib/stores/AuthorizationStore";
import { useCurrentUserDecorationsStore } from "./lib/stores/CurrentUserDecorationsStore";
import { useUserDecorAvatarDecoration, useUsersDecorationsStore } from "./lib/stores/UsersDecorationsStore";
import { setDecorationGridDecoration, setDecorationGridItem } from "./ui/components";
import DecorSection from "./ui/components/DecorSection";
const { isAnimatedAvatarDecoration } = findByPropsLazy("isAnimatedAvatarDecoration");
export interface AvatarDecoration {
asset: string;
skuId: string;
}
const settings = definePluginSettings({
changeDecoration: {
type: OptionType.COMPONENT,
description: "Change your avatar decoration",
component() {
return <div>
<DecorSection hideTitle hideDivider noMargin />
<Forms.FormText type="description" className={classes(Margins.top8, Margins.bottom8)}>
You can also access Decor decorations from the <Link
href="/settings/profile-customization"
onClick={e => {
e.preventDefault();
closeAllModals();
FluxDispatcher.dispatch({ type: "USER_SETTINGS_MODAL_SET_SECTION", section: "Profile Customization" });
}}
>Profiles</Link> page.
</Forms.FormText>
</div>;
}
}
});
export default definePlugin({
name: "Decor",
description: "Create and use your own custom avatar decorations, or pick your favorite from the presets.",
authors: [Devs.FieryFlames],
patches: [
// Patch MediaResolver to return correct URL for Decor avatar decorations
{
find: "getAvatarDecorationURL:",
replacement: {
match: /(?<=function \i\(\i\){)(?=let{avatarDecoration)/,
replace: "const vcDecorDecoration=$self.getDecorAvatarDecorationURL(arguments[0]);if(vcDecorDecoration)return vcDecorDecoration;"
}
},
// Patch profile customization settings to include Decor section
{
find: "DefaultCustomizationSections",
replacement: {
match: /(?<={user:\i},"decoration"\),)/,
replace: "$self.DecorSection(),"
}
},
// Decoration modal module
{
find: ".decorationGridItem",
replacement: [
{
match: /(?<==)\i=>{let{children.{20,100}decorationGridItem/,
replace: "$self.DecorationGridItem=$&"
},
{
match: /(?<==)\i=>{let{user:\i,avatarDecoration.{300,600}decorationGridItemChurned/,
replace: "$self.DecorationGridDecoration=$&"
},
// Remove NEW label from decor avatar decorations
{
match: /(?<=\.Section\.PREMIUM_PURCHASE&&\i;if\()(?<=avatarDecoration:(\i).+?)/,
replace: "$1.skuId===$self.SKU_ID||"
}
]
},
{
find: "isAvatarDecorationAnimating:",
group: true,
replacement: [
// Add Decor avatar decoration hook to avatar decoration hook
{
match: /(?<=TryItOut:\i}\),)(?<=user:(\i).+?)/,
replace: "vcDecorAvatarDecoration=$self.useUserDecorAvatarDecoration($1),"
},
// Use added hook
{
match: /(?<={avatarDecoration:).{1,20}?(?=,)(?<=avatarDecorationOverride:(\i).+?)/,
replace: "$1??vcDecorAvatarDecoration??($&)"
},
// Make memo depend on added hook
{
match: /(?<=size:\i}\),\[)/,
replace: "vcDecorAvatarDecoration,"
}
]
},
// Current user area, at bottom of channels/dm list
{
find: "renderAvatarWithPopout(){",
replacement: [
// Use Decor avatar decoration hook
{
match: /(?<=getAvatarDecorationURL\)\({avatarDecoration:)(\i).avatarDecoration(?=,)/,
replace: "$self.useUserDecorAvatarDecoration($1)??$&"
}
]
}
],
settings,
flux: {
CONNECTION_OPEN: () => {
useAuthorizationStore.getState().init();
useCurrentUserDecorationsStore.getState().clear();
useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true);
},
USER_PROFILE_MODAL_OPEN: data => {
useUsersDecorationsStore.getState().fetch(data.userId, true);
},
},
set DecorationGridItem(e: any) {
setDecorationGridItem(e);
},
set DecorationGridDecoration(e: any) {
setDecorationGridDecoration(e);
},
SKU_ID,
useUserDecorAvatarDecoration,
async start() {
useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true);
},
getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) {
// Only Decor avatar decorations have this SKU ID
if (avatarDecoration?.skuId === SKU_ID) {
const url = new URL(`${CDN_URL}/${avatarDecoration.asset}.png`);
url.searchParams.set("animate", (!!canAnimate && isAnimatedAvatarDecoration(avatarDecoration.asset)).toString());
return url.toString();
} else if (avatarDecoration?.skuId === RAW_SKU_ID) {
return avatarDecoration.asset;
}
},
DecorSection: ErrorBoundary.wrap(DecorSection)
});

View file

@ -0,0 +1,83 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { API_URL } from "./constants";
import { useAuthorizationStore } from "./stores/AuthorizationStore";
export interface Preset {
id: string;
name: string;
description: string | null;
decorations: Decoration[];
authorIds: string[];
}
export interface Decoration {
hash: string;
animated: boolean;
alt: string | null;
authorId: string | null;
reviewed: boolean | null;
presetId: string | null;
}
export interface NewDecoration {
file: File;
alt: string | null;
}
export async function fetchApi(url: RequestInfo, options?: RequestInit) {
const res = await fetch(url, {
...options,
headers: {
...options?.headers,
Authorization: `Bearer ${useAuthorizationStore.getState().token}`
}
});
if (res.ok) return res;
else throw new Error(await res.text());
}
export const getUsersDecorations = async (ids?: string[]): Promise<Record<string, string | null>> => {
if (ids?.length === 0) return {};
const url = new URL(API_URL + "/users");
if (ids && ids.length !== 0) url.searchParams.set("ids", JSON.stringify(ids));
return await fetch(url).then(c => c.json());
};
export const getUserDecorations = async (id: string = "@me"): Promise<Decoration[]> =>
fetchApi(API_URL + `/users/${id}/decorations`).then(c => c.json());
export const getUserDecoration = async (id: string = "@me"): Promise<Decoration | null> =>
fetchApi(API_URL + `/users/${id}/decoration`).then(c => c.json());
export const setUserDecoration = async (decoration: Decoration | NewDecoration | null, id: string = "@me"): Promise<string | Decoration> => {
const formData = new FormData();
if (!decoration) {
formData.append("hash", "null");
} else if ("hash" in decoration) {
formData.append("hash", decoration.hash);
} else if ("file" in decoration) {
formData.append("image", decoration.file);
formData.append("alt", decoration.alt ?? "null");
}
return fetchApi(API_URL + `/users/${id}/decoration`, { method: "PUT", body: formData }).then(c =>
decoration && "file" in decoration ? c.json() : c.text()
);
};
export const getDecoration = async (hash: string): Promise<Decoration> => fetch(API_URL + `/decorations/${hash}`).then(c => c.json());
export const deleteDecoration = async (hash: string): Promise<void> => {
await fetchApi(API_URL + `/decorations/${hash}`, { method: "DELETE" });
};
export const getPresets = async (): Promise<Preset[]> => fetch(API_URL + "/decorations/presets").then(c => c.json());

View file

@ -0,0 +1,16 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export const BASE_URL = "https://decor.fieryflames.dev";
export const API_URL = BASE_URL + "/api";
export const AUTHORIZE_URL = API_URL + "/authorize";
export const CDN_URL = "https://ugc.decor.fieryflames.dev";
export const CLIENT_ID = "1096966363416899624";
export const SKU_ID = "100101099111114"; // decor in ascii numbers
export const RAW_SKU_ID = "11497119"; // raw in ascii numbers
export const GUILD_ID = "1096357702931841148";
export const INVITE_KEY = "dXp2SdxDcP";
export const DECORATION_FETCH_COOLDOWN = 1000 * 60 * 60 * 4; // 4 hours

View file

@ -0,0 +1,102 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { OAuth2AuthorizeModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from "@webpack/common";
import type { StateStorage } from "zustand/middleware";
import { AUTHORIZE_URL, CLIENT_ID } from "../constants";
interface AuthorizationState {
token: string | null;
tokens: Record<string, string>;
init: () => void;
authorize: () => Promise<void>;
setToken: (token: string) => void;
remove: (id: string) => void;
isAuthorized: () => boolean;
}
const indexedDBStorage: StateStorage = {
async getItem(name: string): Promise<string | null> {
return DataStore.get(name).then(v => v ?? null);
},
async setItem(name: string, value: string): Promise<void> {
await DataStore.set(name, value);
},
async removeItem(name: string): Promise<void> {
await DataStore.del(name);
},
};
// TODO: Move switching accounts subscription inside the store?
export const useAuthorizationStore = proxyLazy(() => zustandCreate<AuthorizationState>(
zustandPersist(
(set, get) => ({
token: null,
tokens: {},
init: () => { set({ token: get().tokens[UserStore.getCurrentUser().id] ?? null }); },
setToken: (token: string) => set({ token, tokens: { ...get().tokens, [UserStore.getCurrentUser().id]: token } }),
remove: (id: string) => {
const { tokens, init } = get();
const newTokens = { ...tokens };
delete newTokens[id];
set({ tokens: newTokens });
init();
},
async authorize() {
return new Promise((resolve, reject) => openModal(props =>
<OAuth2AuthorizeModal
{...props}
scopes={["identify"]}
responseType="code"
redirectUri={AUTHORIZE_URL}
permissions={0n}
clientId={CLIENT_ID}
cancelCompletesFlow={false}
callback={async (response: any) => {
try {
const url = new URL(response.location);
url.searchParams.append("client", "vencord");
const req = await fetch(url);
if (req?.ok) {
const token = await req.text();
get().setToken(token);
} else {
throw new Error("Request not OK");
}
resolve(void 0);
} catch (e) {
if (e instanceof Error) {
showToast(`Failed to authorize: ${e.message}`, Toasts.Type.FAILURE);
new Logger("Decor").error("Failed to authorize", e);
reject(e);
}
}
}}
/>, {
onCloseCallback() {
reject(new Error("Authorization cancelled"));
},
}
));
},
isAuthorized: () => !!get().token,
}),
{
name: "decor-auth",
getStorage: () => indexedDBStorage,
partialize: state => ({ tokens: state.tokens }),
onRehydrateStorage: () => state => state?.init()
}
)
));

View file

@ -0,0 +1,56 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { proxyLazy } from "@utils/lazy";
import { UserStore, zustandCreate } from "@webpack/common";
import { Decoration, deleteDecoration, getUserDecoration, getUserDecorations, NewDecoration, setUserDecoration } from "../api";
import { decorationToAsset } from "../utils/decoration";
import { useUsersDecorationsStore } from "./UsersDecorationsStore";
interface UserDecorationsState {
decorations: Decoration[];
selectedDecoration: Decoration | null;
fetch: () => Promise<void>;
delete: (decoration: Decoration | string) => Promise<void>;
create: (decoration: NewDecoration) => Promise<void>;
select: (decoration: Decoration | null) => Promise<void>;
clear: () => void;
}
export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate<UserDecorationsState>((set, get) => ({
decorations: [],
selectedDecoration: null,
async fetch() {
const decorations = await getUserDecorations();
const selectedDecoration = await getUserDecoration();
set({ decorations, selectedDecoration });
},
async create(newDecoration: NewDecoration) {
const decoration = (await setUserDecoration(newDecoration)) as Decoration;
set({ decorations: [...get().decorations, decoration] });
},
async delete(decoration: Decoration | string) {
const hash = typeof decoration === "object" ? decoration.hash : decoration;
await deleteDecoration(hash);
const { selectedDecoration, decorations } = get();
const newState = {
decorations: decorations.filter(d => d.hash !== hash),
selectedDecoration: selectedDecoration?.hash === hash ? null : selectedDecoration
};
set(newState);
},
async select(decoration: Decoration | null) {
if (get().selectedDecoration === decoration) return;
set({ selectedDecoration: decoration });
setUserDecoration(decoration);
useUsersDecorationsStore.getState().set(UserStore.getCurrentUser().id, decoration ? decorationToAsset(decoration) : null);
},
clear: () => set({ decorations: [], selectedDecoration: null })
})));

View file

@ -0,0 +1,118 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { debounce } from "@utils/debounce";
import { proxyLazy } from "@utils/lazy";
import { useEffect, useState, zustandCreate } from "@webpack/common";
import { User } from "discord-types/general";
import { AvatarDecoration } from "../../";
import { getUsersDecorations } from "../api";
import { DECORATION_FETCH_COOLDOWN, SKU_ID } from "../constants";
interface UserDecorationData {
asset: string | null;
fetchedAt: Date;
}
interface UsersDecorationsState {
usersDecorations: Map<string, UserDecorationData>;
fetchQueue: Set<string>;
bulkFetch: () => Promise<void>;
fetch: (userId: string, force?: boolean) => Promise<void>;
fetchMany: (userIds: string[]) => Promise<void>;
get: (userId: string) => UserDecorationData | undefined;
getAsset: (userId: string) => string | null | undefined;
has: (userId: string) => boolean;
set: (userId: string, decoration: string | null) => void;
}
export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecorationsState>((set, get) => ({
usersDecorations: new Map<string, UserDecorationData>(),
fetchQueue: new Set(),
bulkFetch: debounce(async () => {
const { fetchQueue, usersDecorations } = get();
if (fetchQueue.size === 0) return;
set({ fetchQueue: new Set() });
const fetchIds = Array.from(fetchQueue);
const fetchedUsersDecorations = await getUsersDecorations(fetchIds);
const newUsersDecorations = new Map(usersDecorations);
const now = new Date();
for (const fetchId of fetchIds) {
const newDecoration = fetchedUsersDecorations[fetchId] ?? null;
newUsersDecorations.set(fetchId, { asset: newDecoration, fetchedAt: now });
}
set({ usersDecorations: newUsersDecorations });
}),
async fetch(userId: string, force: boolean = false) {
const { usersDecorations, fetchQueue, bulkFetch } = get();
const { fetchedAt } = usersDecorations.get(userId) ?? {};
if (fetchedAt) {
if (!force && Date.now() - fetchedAt.getTime() < DECORATION_FETCH_COOLDOWN) return;
}
set({ fetchQueue: new Set(fetchQueue).add(userId) });
bulkFetch();
},
async fetchMany(userIds) {
if (!userIds.length) return;
const { usersDecorations, fetchQueue, bulkFetch } = get();
const newFetchQueue = new Set(fetchQueue);
const now = Date.now();
for (const userId of userIds) {
const { fetchedAt } = usersDecorations.get(userId) ?? {};
if (fetchedAt) {
if (now - fetchedAt.getTime() < DECORATION_FETCH_COOLDOWN) continue;
}
newFetchQueue.add(userId);
}
set({ fetchQueue: newFetchQueue });
bulkFetch();
},
get(userId: string) { return get().usersDecorations.get(userId); },
getAsset(userId: string) { return get().usersDecorations.get(userId)?.asset; },
has(userId: string) { return get().usersDecorations.has(userId); },
set(userId: string, decoration: string | null) {
const { usersDecorations } = get();
const newUsersDecorations = new Map(usersDecorations);
newUsersDecorations.set(userId, { asset: decoration, fetchedAt: new Date() });
set({ usersDecorations: newUsersDecorations });
}
})));
export function useUserDecorAvatarDecoration(user?: User): AvatarDecoration | null | undefined {
const [decorAvatarDecoration, setDecorAvatarDecoration] = useState<string | null>(user ? useUsersDecorationsStore.getState().getAsset(user.id) ?? null : null);
useEffect(() => {
const destructor = useUsersDecorationsStore.subscribe(
state => {
if (!user) return;
const newDecorAvatarDecoration = state.getAsset(user.id);
if (!newDecorAvatarDecoration) return;
if (decorAvatarDecoration !== newDecorAvatarDecoration) setDecorAvatarDecoration(newDecorAvatarDecoration);
}
);
if (user) {
const { fetch: fetchUserDecorAvatarDecoration } = useUsersDecorationsStore.getState();
fetchUserDecorAvatarDecoration(user.id);
}
return destructor;
}, []);
return decorAvatarDecoration ? { asset: decorAvatarDecoration, skuId: SKU_ID } : null;
}

View file

@ -0,0 +1,17 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { AvatarDecoration } from "../../";
import { Decoration } from "../api";
import { SKU_ID } from "../constants";
export function decorationToAsset(decoration: Decoration) {
return `${decoration.animated ? "a_" : ""}${decoration.hash}`;
}
export function decorationToAvatarDecoration(decoration: Decoration): AvatarDecoration {
return { asset: decorationToAsset(decoration), skuId: SKU_ID };
}

View file

@ -0,0 +1,35 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ContextMenuApi } from "@webpack/common";
import type { HTMLProps } from "react";
import { Decoration } from "../../lib/api";
import { decorationToAvatarDecoration } from "../../lib/utils/decoration";
import { DecorationGridDecoration } from ".";
import DecorationContextMenu from "./DecorationContextMenu";
interface DecorDecorationGridDecorationProps extends HTMLProps<HTMLDivElement> {
decoration: Decoration;
isSelected: boolean;
onSelect: () => void;
}
export default function DecorDecorationGridDecoration(props: DecorDecorationGridDecorationProps) {
const { decoration } = props;
return <DecorationGridDecoration
{...props}
onContextMenu={e => {
ContextMenuApi.openContextMenu(e, () => (
<DecorationContextMenu
decoration={decoration}
/>
));
}}
avatarDecoration={decorationToAvatarDecoration(decoration)}
/>;
}

View file

@ -0,0 +1,59 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Flex } from "@components/Flex";
import { findByCodeLazy } from "@webpack";
import { Button, useEffect } from "@webpack/common";
import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore";
import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore";
import { cl } from "../";
import { openChangeDecorationModal } from "../modals/ChangeDecorationModal";
const CustomizationSection = findByCodeLazy(".customizationSectionBackground");
interface DecorSectionProps {
hideTitle?: boolean;
hideDivider?: boolean;
noMargin?: boolean;
}
export default function DecorSection({ hideTitle = false, hideDivider = false, noMargin = false }: DecorSectionProps) {
const authorization = useAuthorizationStore();
const { selectedDecoration, select: selectDecoration, fetch: fetchDecorations } = useCurrentUserDecorationsStore();
useEffect(() => {
if (authorization.isAuthorized()) fetchDecorations();
}, [authorization.token]);
return <CustomizationSection
title={!hideTitle && "Decor"}
hasBackground={true}
hideDivider={hideDivider}
className={noMargin && cl("section-remove-margin")}
>
<Flex>
<Button
onClick={() => {
if (!authorization.isAuthorized()) {
authorization.authorize().then(openChangeDecorationModal).catch(() => { });
} else openChangeDecorationModal();
}}
size={Button.Sizes.SMALL}
>
Change Decoration
</Button>
{selectedDecoration && authorization.isAuthorized() && <Button
onClick={() => selectDecoration(null)}
color={Button.Colors.PRIMARY}
size={Button.Sizes.SMALL}
look={Button.Looks.LINK}
>
Remove Decoration
</Button>}
</Flex>
</CustomizationSection>;
}

View file

@ -0,0 +1,47 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CopyIcon, DeleteIcon } from "@components/Icons";
import { Alerts, Clipboard, ContextMenuApi, Menu, UserStore } from "webpack/common";
import { Decoration } from "../../lib/api";
import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore";
import { cl } from "../";
export default function DecorationContextMenu({ decoration }: { decoration: Decoration; }) {
const { delete: deleteDecoration } = useCurrentUserDecorationsStore();
return <Menu.Menu
navId={cl("decoration-context-menu")}
onClose={ContextMenuApi.closeContextMenu}
aria-label="Decoration Options"
>
<Menu.MenuItem
id={cl("decoration-context-menu-copy-hash")}
label="Copy Decoration Hash"
icon={CopyIcon}
action={() => Clipboard.copy(decoration.hash)}
/>
{decoration.authorId === UserStore.getCurrentUser().id &&
<Menu.MenuItem
id={cl("decoration-context-menu-delete")}
label="Delete Decoration"
color="danger"
icon={DeleteIcon}
action={() => Alerts.show({
title: "Delete Decoration",
body: `Are you sure you want to delete ${decoration.alt}?`,
confirmText: "Delete",
confirmColor: cl("danger-btn"),
cancelText: "Cancel",
onConfirm() {
deleteDecoration(decoration);
}
})}
/>
}
</Menu.Menu>;
}

View file

@ -0,0 +1,30 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { PlusIcon } from "@components/Icons";
import { i18n, Text } from "@webpack/common";
import { HTMLProps } from "react";
import { DecorationGridItem } from ".";
type DecorationGridCreateProps = HTMLProps<HTMLDivElement> & {
onSelect: () => void;
};
export default function DecorationGridCreate(props: DecorationGridCreateProps) {
return <DecorationGridItem
{...props}
isSelected={false}
>
<PlusIcon />
<Text
variant="text-xs/normal"
color="header-primary"
>
{i18n.Messages.CREATE}
</Text>
</DecorationGridItem >;
}

View file

@ -0,0 +1,30 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NoEntrySignIcon } from "@components/Icons";
import { i18n, Text } from "@webpack/common";
import { HTMLProps } from "react";
import { DecorationGridItem } from ".";
type DecorationGridNoneProps = HTMLProps<HTMLDivElement> & {
isSelected: boolean;
onSelect: () => void;
};
export default function DecorationGridNone(props: DecorationGridNoneProps) {
return <DecorationGridItem
{...props}
>
<NoEntrySignIcon />
<Text
variant="text-xs/normal"
color="header-primary"
>
{i18n.Messages.NONE}
</Text>
</DecorationGridItem >;
}

View file

@ -0,0 +1,28 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { React } from "@webpack/common";
import { cl } from "../";
export interface GridProps<ItemT> {
renderItem: (item: ItemT) => JSX.Element;
getItemKey: (item: ItemT) => string;
itemKeyPrefix?: string;
items: Array<ItemT>;
}
export default function Grid<ItemT,>({ renderItem, getItemKey, itemKeyPrefix: ikp, items }: GridProps<ItemT>) {
return <div className={cl("sectioned-grid-list-grid")}>
{items.map(item =>
<React.Fragment
key={`${ikp ? `${ikp}-` : ""}${getItemKey(item)}`}
>
{renderItem(item)}
</React.Fragment>
)}
</div>;
}

View file

@ -0,0 +1,38 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classes } from "@utils/misc";
import { findByPropsLazy } from "@webpack";
import { React } from "@webpack/common";
import { cl } from "../";
import Grid, { GridProps } from "./Grid";
const ScrollerClasses = findByPropsLazy("managedReactiveScroller");
type Section<SectionT, ItemT> = SectionT & {
items: Array<ItemT>;
};
interface SectionedGridListProps<ItemT, SectionT, SectionU = Section<SectionT, ItemT>> extends Omit<GridProps<ItemT>, "items"> {
renderSectionHeader: (section: SectionU) => JSX.Element;
getSectionKey: (section: SectionU) => string;
sections: SectionU[];
}
export default function SectionedGridList<ItemT, SectionU,>(props: SectionedGridListProps<ItemT, SectionU>) {
return <div className={classes(cl("sectioned-grid-list-container"), ScrollerClasses.thin)}>
{props.sections.map(section => <div key={props.getSectionKey(section)} className={cl("sectioned-grid-list-section")}>
{props.renderSectionHeader(section)}
<Grid
renderItem={props.renderItem}
getItemKey={props.getItemKey}
itemKeyPrefix={props.getSectionKey(section)}
items={section.items}
/>
</div>)}
</div>;
}

View file

@ -0,0 +1,33 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findComponentByCode, LazyComponentWebpack } from "@webpack";
import { React } from "@webpack/common";
import type { ComponentType, HTMLProps, PropsWithChildren } from "react";
import { AvatarDecoration } from "../..";
type DecorationGridItemComponent = ComponentType<PropsWithChildren<HTMLProps<HTMLDivElement>> & {
onSelect: () => void,
isSelected: boolean,
}>;
export let DecorationGridItem: DecorationGridItemComponent;
export const setDecorationGridItem = v => DecorationGridItem = v;
export const AvatarDecorationModalPreview = LazyComponentWebpack(() => {
const component = findComponentByCode("AvatarDecorationModalPreview");
return React.memo(component);
});
type DecorationGridDecorationComponent = React.ComponentType<HTMLProps<HTMLDivElement> & {
avatarDecoration: AvatarDecoration;
onSelect: () => void,
isSelected: boolean,
}>;
export let DecorationGridDecoration: DecorationGridDecorationComponent;
export const setDecorationGridDecoration = v => DecorationGridDecoration = v;

View file

@ -0,0 +1,13 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import { extractAndLoadChunksLazy } from "@webpack";
export const cl = classNameFactory("vc-decor-");
export const requireAvatarDecorationModal = extractAndLoadChunksLazy(["openAvatarDecorationModal:"]);
export const requireCreateStickerModal = extractAndLoadChunksLazy(["stickerInspected]:"]);

View file

@ -0,0 +1,270 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Flex } from "@components/Flex";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common";
import { User } from "discord-types/general";
import { Decoration, getPresets, Preset } from "../../lib/api";
import { GUILD_ID, INVITE_KEY } from "../../lib/constants";
import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore";
import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore";
import { decorationToAvatarDecoration } from "../../lib/utils/decoration";
import { cl, requireAvatarDecorationModal } from "../";
import { AvatarDecorationModalPreview } from "../components";
import DecorationGridCreate from "../components/DecorationGridCreate";
import DecorationGridNone from "../components/DecorationGridNone";
import DecorDecorationGridDecoration from "../components/DecorDecorationGridDecoration";
import SectionedGridList from "../components/SectionedGridList";
import { openCreateDecorationModal } from "./CreateDecorationModal";
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const DecorationModalStyles = findByPropsLazy("modalFooterShopButton");
function usePresets() {
const [presets, setPresets] = useState<Preset[]>([]);
useEffect(() => { getPresets().then(setPresets); }, []);
return presets;
}
interface Section {
title: string;
subtitle?: string;
sectionKey: string;
items: ("none" | "create" | Decoration)[];
authorIds?: string[];
}
function SectionHeader({ section }: { section: Section; }) {
const hasSubtitle = typeof section.subtitle !== "undefined";
const hasAuthorIds = typeof section.authorIds !== "undefined";
const [authors, setAuthors] = useState<User[]>([]);
useEffect(() => {
(async () => {
if (!section.authorIds) return;
for (const authorId of section.authorIds) {
const author = UserStore.getUser(authorId) ?? await UserUtils.getUser(authorId);
setAuthors(authors => [...authors, author]);
}
})();
}, [section.authorIds]);
return <div>
<Flex>
<Forms.FormTitle style={{ flexGrow: 1 }}>{section.title}</Forms.FormTitle>
{hasAuthorIds && <UserSummaryItem
users={authors}
guildId={undefined}
renderIcon={false}
max={5}
showDefaultAvatarsForNullUsers
size={16}
showUserPopout
className={Margins.bottom8}
/>
}
</Flex>
{hasSubtitle &&
<Forms.FormText type="description" className={Margins.bottom8}>
{section.subtitle}
</Forms.FormText>
}
</div>;
}
export default function ChangeDecorationModal(props: any) {
// undefined = not trying, null = none, Decoration = selected
const [tryingDecoration, setTryingDecoration] = useState<Decoration | null | undefined>(undefined);
const isTryingDecoration = typeof tryingDecoration !== "undefined";
const avatarDecorationOverride = tryingDecoration != null ? decorationToAvatarDecoration(tryingDecoration) : tryingDecoration;
const {
decorations,
selectedDecoration,
fetch: fetchUserDecorations,
select: selectDecoration
} = useCurrentUserDecorationsStore();
useEffect(() => {
fetchUserDecorations();
}, []);
const activeSelectedDecoration = isTryingDecoration ? tryingDecoration : selectedDecoration;
const activeDecorationHasAuthor = typeof activeSelectedDecoration?.authorId !== "undefined";
const hasDecorationPendingReview = decorations.some(d => d.reviewed === false);
const presets = usePresets();
const presetDecorations = presets.flatMap(preset => preset.decorations);
const activeDecorationPreset = presets.find(preset => preset.id === activeSelectedDecoration?.presetId);
const isActiveDecorationPreset = typeof activeDecorationPreset !== "undefined";
const ownDecorations = decorations.filter(d => !presetDecorations.some(p => p.hash === d.hash));
const data = [
{
title: "Your Decorations",
sectionKey: "ownDecorations",
items: ["none", ...ownDecorations, "create"]
},
...presets.map(preset => ({
title: preset.name,
subtitle: preset.description || undefined,
sectionKey: `preset-${preset.id}`,
items: preset.decorations,
authorIds: preset.authorIds
}))
] as Section[];
return <ModalRoot
{...props}
size={ModalSize.DYNAMIC}
className={DecorationModalStyles.modal}
>
<ModalHeader separator={false} className={cl("modal-header")}>
<Text
color="header-primary"
variant="heading-lg/semibold"
tag="h1"
style={{ flexGrow: 1 }}
>
Change Decoration
</Text>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent
className={cl("change-decoration-modal-content")}
scrollbarType="none"
>
<SectionedGridList
renderItem={item => {
if (typeof item === "string") {
switch (item) {
case "none":
return <DecorationGridNone
className={cl("change-decoration-modal-decoration")}
isSelected={activeSelectedDecoration === null}
onSelect={() => setTryingDecoration(null)}
/>;
case "create":
return <Tooltip text="You already have a decoration pending review" shouldShow={hasDecorationPendingReview}>
{tooltipProps => <DecorationGridCreate
className={cl("change-decoration-modal-decoration")}
{...tooltipProps}
onSelect={!hasDecorationPendingReview ? openCreateDecorationModal : () => { }}
/>}
</Tooltip>;
}
} else {
return <Tooltip text={"Pending review"} shouldShow={item.reviewed === false}>
{tooltipProps => (
<DecorDecorationGridDecoration
{...tooltipProps}
className={cl("change-decoration-modal-decoration")}
onSelect={item.reviewed !== false ? () => setTryingDecoration(item) : () => { }}
isSelected={activeSelectedDecoration?.hash === item.hash}
decoration={item}
/>
)}
</Tooltip>;
}
}}
getItemKey={item => typeof item === "string" ? item : item.hash}
getSectionKey={section => section.sectionKey}
renderSectionHeader={section => <SectionHeader section={section} />}
sections={data}
/>
<div className={cl("change-decoration-modal-preview")}>
<AvatarDecorationModalPreview
avatarDecorationOverride={avatarDecorationOverride}
user={UserStore.getCurrentUser()}
/>
{isActiveDecorationPreset && <Forms.FormTitle className="">Part of the {activeDecorationPreset.name} Preset</Forms.FormTitle>}
{typeof activeSelectedDecoration === "object" &&
<Text
variant="text-sm/semibold"
color="header-primary"
>
{activeSelectedDecoration?.alt}
</Text>
}
{activeDecorationHasAuthor && <Text key={`createdBy-${activeSelectedDecoration.authorId}`}>Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}</Text>}
</div>
</ModalContent>
<ModalFooter className={classes(cl("change-decoration-modal-footer", cl("modal-footer")))}>
<div className={cl("change-decoration-modal-footer-btn-container")}>
<Button
onClick={() => {
selectDecoration(tryingDecoration!).then(props.onClose);
}}
disabled={!isTryingDecoration}
>
Apply
</Button>
<Button
onClick={props.onClose}
color={Button.Colors.PRIMARY}
look={Button.Looks.LINK}
>
Cancel
</Button>
</div>
<div className={cl("change-decoration-modal-footer-btn-container")}>
<Button
onClick={() => Alerts.show({
title: "Log Out",
body: "Are you sure you want to log out of Decor?",
confirmText: "Log Out",
confirmColor: cl("danger-btn"),
cancelText: "Cancel",
onConfirm() {
useAuthorizationStore.getState().remove(UserStore.getCurrentUser().id);
props.onClose();
}
})}
color={Button.Colors.PRIMARY}
look={Button.Looks.LINK}
>
Log Out
</Button>
<Tooltip text="Join Decor's Discord Server for notifications on your decoration's review, and when new presets are released">
{tooltipProps => <Button
{...tooltipProps}
onClick={async () => {
if (!GuildStore.getGuild(GUILD_ID)) {
const inviteAccepted = await openInviteModal(INVITE_KEY);
if (inviteAccepted) {
closeAllModals();
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
}
} else {
props.onClose();
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
NavigationRouter.transitionToGuild(GUILD_ID);
}
}}
color={Button.Colors.PRIMARY}
look={Button.Looks.LINK}
>
Discord Server
</Button>}
</Tooltip>
</div>
</ModalFooter>
</ModalRoot>;
}
export const openChangeDecorationModal = () =>
requireAvatarDecorationModal().then(() => openModal(props => <ChangeDecorationModal {...props} />));

View file

@ -0,0 +1,163 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Link } from "@components/Link";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common";
import { GUILD_ID, INVITE_KEY, RAW_SKU_ID } from "../../lib/constants";
import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore";
import { cl, requireAvatarDecorationModal, requireCreateStickerModal } from "../";
import { AvatarDecorationModalPreview } from "../components";
const DecorationModalStyles = findByPropsLazy("modalFooterShopButton");
const FileUpload = findComponentByCodeLazy("fileUploadInput,");
function useObjectURL(object: Blob | MediaSource | null) {
const [url, setUrl] = useState<string | null>(null);
useEffect(() => {
if (!object) return;
const objectUrl = URL.createObjectURL(object);
setUrl(objectUrl);
return () => {
URL.revokeObjectURL(objectUrl);
setUrl(null);
};
}, [object]);
return url;
}
export default function CreateDecorationModal(props) {
const [name, setName] = useState("");
const [file, setFile] = useState<File | null>(null);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (error) setError(null);
}, [file]);
const { create: createDecoration } = useCurrentUserDecorationsStore();
const fileUrl = useObjectURL(file);
const decoration = useMemo(() => fileUrl ? { asset: fileUrl, skuId: RAW_SKU_ID } : null, [fileUrl]);
return <ModalRoot
{...props}
size={ModalSize.MEDIUM}
className={DecorationModalStyles.modal}
>
<ModalHeader separator={false} className={cl("modal-header")}>
<Text
color="header-primary"
variant="heading-lg/semibold"
tag="h1"
style={{ flexGrow: 1 }}
>
Create Decoration
</Text>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent
className={cl("create-decoration-modal-content")}
scrollbarType="none"
>
<div className={cl("create-decoration-modal-form-preview-container")}>
<div className={cl("create-decoration-modal-form")}>
{error !== null && <Text color="text-danger" variant="text-xs/normal">{error.message}</Text>}
<Forms.FormSection title="File">
<FileUpload
filename={file?.name}
placeholder="Choose a file"
buttonText="Browse"
filters={[{ name: "Decoration file", extensions: ["png", "apng"] }]}
onFileSelect={setFile}
/>
<Forms.FormText type="description" className={Margins.top8}>
File should be APNG or PNG.
</Forms.FormText>
</Forms.FormSection>
<Forms.FormSection title="Name">
<TextInput
placeholder="Companion Cube"
value={name}
onChange={setName}
/>
<Forms.FormText type="description" className={Margins.top8}>
This name will be used when referring to this decoration.
</Forms.FormText>
</Forms.FormSection>
</div>
<div>
<AvatarDecorationModalPreview
avatarDecorationOverride={decoration}
user={UserStore.getCurrentUser()}
/>
</div>
</div>
<Forms.FormText type="description" className={Margins.bottom16}>
Make sure your decoration does not violate <Link
href="https://github.com/decor-discord/.github/blob/main/GUIDELINES.md"
>
the guidelines
</Link> before creating your decoration.
<br />You can receive updates on your decoration's review by joining <Link
href={`https://discord.gg/${INVITE_KEY}`}
onClick={async e => {
e.preventDefault();
if (!GuildStore.getGuild(GUILD_ID)) {
const inviteAccepted = await openInviteModal(INVITE_KEY);
if (inviteAccepted) {
closeAllModals();
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
}
} else {
closeAllModals();
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
NavigationRouter.transitionToGuild(GUILD_ID);
}
}}
>
Decor's Discord server
</Link>.
</Forms.FormText>
</ModalContent>
<ModalFooter className={cl("modal-footer")}>
<Button
onClick={() => {
setSubmitting(true);
createDecoration({ alt: name, file: file! })
.then(props.onClose).catch(e => { setSubmitting(false); setError(e); });
}}
disabled={!file || !name}
submitting={submitting}
>
Create
</Button>
<Button
onClick={props.onClose}
color={Button.Colors.PRIMARY}
look={Button.Looks.LINK}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
}
export const openCreateDecorationModal = () =>
Promise.all([requireAvatarDecorationModal(), requireCreateStickerModal()])
.then(() => openModal(props => <CreateDecorationModal {...props} />));

View file

@ -0,0 +1,80 @@
.vc-decor-danger-btn {
color: var(--white-500);
background-color: var(--button-danger-background);
}
.vc-decor-change-decoration-modal-content {
position: relative;
display: flex;
border-radius: 5px 5px 0 0;
padding: 0 16px;
gap: 4px
}
.vc-decor-change-decoration-modal-preview {
display: flex;
flex-direction: column;
margin-top: 24px;
gap: 8px;
max-width: 280px;
}
.vc-decor-change-decoration-modal-decoration {
width: 80px;
height: 80px;
}
.vc-decor-change-decoration-modal-footer {
justify-content: space-between;
}
.vc-decor-change-decoration-modal-footer-btn-container {
display: flex;
flex-direction: row-reverse;
}
.vc-decor-create-decoration-modal-content {
display: flex;
flex-direction: column;
gap: 20px;
padding: 0 16px;
}
.vc-decor-create-decoration-modal-form-preview-container {
display: flex;
gap: 16px;
}
.vc-decor-modal-header {
padding: 16px;
}
.vc-decor-modal-footer {
padding: 16px;
}
.vc-decor-create-decoration-modal-form {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: 16px;
}
.vc-decor-sectioned-grid-list-container {
display: flex;
flex-direction: column;
overflow: hidden scroll;
max-height: 512px;
width: 352px; /* ((80 + 8 (grid gap)) * desired columns) (scrolled takes the extra 8 padding off conveniently) */
gap: 12px;
}
.vc-decor-sectioned-grid-list-grid {
display: flex;
flex-wrap: wrap;
gap: 8px
}
.vc-decor-section-remove-margin {
margin-bottom: 0;
}

View file

@ -215,6 +215,9 @@ function initWs(isManual = false) {
case "ModuleId":
results = Object.keys(search(parsedArgs[0]));
break;
case "ComponentByCode":
results = findAll(filters.componentByCode(...parsedArgs));
break;
default:
return reply("Unknown Find Type " + type);
}

View file

@ -21,10 +21,9 @@ import { definePluginSettings, Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
import { getCurrentGuild } from "@utils/discord";
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
@ -48,9 +47,9 @@ function searchProtoClassField(localName: string, protoClass: any) {
return fieldGetter?.();
}
const PreloadedUserSettingsActionCreators = proxyLazy(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators);
const AppearanceSettingsActionCreators = proxyLazy(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));
const ClientThemeSettingsActionsCreators = proxyLazy(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));
const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators);
const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));
const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));
const USE_EXTERNAL_EMOJIS = 1n << 18n;
const USE_EXTERNAL_STICKERS = 1n << 37n;
@ -360,7 +359,7 @@ export default definePlugin({
},
// Separate patch for allowing using custom app icons
{
find: "location:\"AppIconHome\"",
find: ".FreemiumAppIconIds.DEFAULT&&(",
replacement: {
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
replace: "true"
@ -788,7 +787,14 @@ export default definePlugin({
if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId))
break stickerBypass;
const link = this.getStickerLink(sticker.id);
// [12/12/2023]
// Work around an annoying bug where getStickerLink will return StickerType.GIF,
// but will give us a normal non animated png for no reason
// TODO: Remove this workaround when it's not needed anymore
let link = this.getStickerLink(sticker.id);
if (sticker.format_type === StickerType.GIF && link.includes(".png")) {
link = link.replace(".png", ".gif");
}
if (sticker.format_type === StickerType.APNG) {
this.sendAnimatedSticker(link, sticker.id, channelId);
return { cancel: true };

View file

@ -0,0 +1,25 @@
/*
* 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\){.+?return )\i\.SUPPORTS_WEBP.+?:(?=\i&&\(\i="png"\))/,
replace: ""
}
]
}
]
});

View file

@ -28,21 +28,22 @@ import style from "./style.css?managed";
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:");
function makeIcon(showCurrentGame?: boolean) {
const controllerIcon = "M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z";
return function () {
return (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
>
<path fill="currentColor" mask="url(#gameActivityMask)" d="M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z" />
{!showCurrentGame && <>
<svg width="20" height="20" viewBox="0 0 24 24">
{showCurrentGame ? (
<path fill="currentColor" d={controllerIcon} />
) : (
<>
<mask id="gameActivityMask" >
<rect fill="white" x="0" y="0" width="24" height="24" />
<path fill="black" d="M23.27 4.54 19.46.73 .73 19.46 4.54 23.27 23.27 4.54Z" />
<path fill="black" d="M23.27 4.73 19.27 .73 -.27 20.27 3.73 24.27Z" />
</mask>
<path fill="var(--status-danger)" d="M23 2.27 21.73 1 1 21.73 2.27 23 23 2.27Z" />
</>}
<path fill="var(--status-danger)" mask="url(#gameActivityMask)" d={controllerIcon} />
<path fill="var(--status-danger)" d="M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4Z" />
</>
)}
</svg>
);
};

View file

@ -20,7 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ContextMenuApi, FluxDispatcher, Menu } from "@webpack/common";
import { ContextMenuApi, FluxDispatcher, Menu, MessageActions } from "@webpack/common";
import { Channel, Message } from "discord-types/general";
interface Sticker {
@ -49,7 +49,6 @@ const settings = definePluginSettings({
unholyMultiGreetEnabled?: boolean;
}>();
const MessageActions = findByPropsLazy("sendGreetMessage");
const { WELCOME_STICKERS } = findByPropsLazy("WELCOME_STICKERS");
function greet(channel: Channel, message: Message, stickers: string[]) {

View file

@ -22,7 +22,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "iLoveSpam",
description: "Do not hide messages from 'likely spammers'",
authors: [Devs.botato, Devs.Animal],
authors: [Devs.botato, Devs.Nyako],
patches: [
{
find: "hasFlag:{writable",

View file

@ -72,6 +72,7 @@ export default definePlugin({
if (event.detail < 2) return;
if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return;
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return;
if (msg.deleted === true) return;
if (isMe) {
if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id)) return;
@ -81,6 +82,9 @@ export default definePlugin({
} else {
if (!settings.store.enableDoubleClickToReply) return;
const EPHEMERAL = 64;
if (msg.hasFlag(EPHEMERAL)) return;
FluxDispatcher.dispatch({
type: "CREATE_PENDING_REPLY",
channel,

View file

@ -19,7 +19,9 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByProps } from "@webpack";
import { findByPropsLazy } from "@webpack";
const { updateGuildNotificationSettings } = findByPropsLazy("updateGuildNotificationSettings");
const settings = definePluginSettings({
guild: {
@ -63,7 +65,7 @@ export default definePlugin({
handleMute(guildId: string | null) {
if (guildId === "@me" || guildId === "null" || guildId == null) return;
findByProps("updateGuildNotificationSettings").updateGuildNotificationSettings(guildId,
updateGuildNotificationSettings(guildId,
{
muted: settings.store.guild,
suppress_everyone: settings.store.everyone,

View file

@ -0,0 +1,3 @@
# NotificationVolume
Set a separate volume for notifications and in-app sounds (e.g. messages, call sound, mute/unmute) helping your ears stay healthy for many years to come.

View file

@ -0,0 +1,35 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
notificationVolume: {
type: OptionType.SLIDER,
description: "Notification volume",
markers: [0, 25, 50, 75, 100],
default: 100,
stickToMarkers: false
}
});
export default definePlugin({
name: "NotificationVolume",
description: "Save your ears and set a separate volume for notifications and in-app sounds",
authors: [Devs.philipbry],
settings,
patches: [
{
find: "_ensureAudio(){",
replacement: {
match: /onloadeddata=\(\)=>\{.\.volume=/,
replace: "$&$self.settings.store.notificationVolume/100*"
},
},
],
});

View file

@ -28,7 +28,8 @@ export default definePlugin({
start() {
fetch("https://raw.githubusercontent.com/adryd325/oneko.js/8fa8a1864aa71cd7a794d58bc139e755e96a236c/oneko.js")
.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")
.replace("(isReducedMotion)", "(false)"))
.then(eval);
},

View file

@ -18,9 +18,8 @@
import ErrorBoundary from "@components/ErrorBoundary";
import ExpandableHeader from "@components/ExpandableHeader";
import { proxyLazy } from "@utils/lazy";
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 type { Guild, GuildMember } from "discord-types/general";
@ -36,15 +35,13 @@ interface UserPermission {
type UserPermissions = Array<UserPermission>;
const Classes = proxyLazy(() => {
const modules = findBulk(
const Classes = proxyLazyWebpack(() =>
Object.assign({}, ...findBulk(
filters.byProps("roles", "rolePill", "rolePillBorder"),
filters.byProps("roleCircle", "dotBorderBase", "dotBorderColor"),
filters.byProps("roleNameOverflow", "root", "roleName", "roleRemoveButton")
);
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, showBorder }: { guild: Guild; guildMember: GuildMember; showBorder: boolean; }) {
const stns = settings.use(["permissionsSortOrder"]);

View file

@ -55,13 +55,13 @@ const Icons = {
};
type Platform = keyof typeof Icons;
const StatusUtils = findByPropsLazy("getStatusColor", "StatusTypes");
const StatusUtils = findByPropsLazy("useStatusFillColor", "StatusTypes");
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
const tooltip = platform[0].toUpperCase() + platform.slice(1);
const Icon = Icons[platform] ?? Icons.desktop;
return <Icon color={`var(--${StatusUtils.getStatusColor(status)}`} tooltip={tooltip} small={small} />;
return <Icon color={StatusUtils.useStatusFillColor(status)} tooltip={tooltip} small={small} />;
};
const getStatus = (id: string): Record<Platform, string> => PresenceStore.getState()?.clientStatuses?.[id];

View file

@ -31,7 +31,7 @@ export default definePlugin({
start() {
addButton("QuickMention", msg => {
const channel = ChannelStore.getChannel(msg.channel_id);
if (!PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null;
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null;
return {
label: "Quick Mention",

View file

@ -18,27 +18,28 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { React } from "@webpack/common";
let ERROR_CODES: any;
const CODES_URL =
"https://raw.githubusercontent.com/facebook/react/17.0.2/scripts/error-codes/codes.json";
export default definePlugin({
name: "ReactErrorDecoder",
description: 'Replaces "Minifed React Error" with the actual error.',
authors: [Devs.Cyn],
authors: [Devs.Cyn, Devs.maisymoe],
patches: [
{
find: '"https://reactjs.org/docs/error-decoder.html?invariant="',
replacement: {
match: /(function .\(.\)){(for\(var .="https:\/\/reactjs\.org\/docs\/error-decoder\.html\?invariant="\+.,.=1;.<arguments\.length;.\+\+\).\+="&args\[\]="\+encodeURIComponent\(arguments\[.\]\);return"Minified React error #"\+.\+"; visit "\+.\+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings.")}/,
replace: (_, func, original) =>
`${func}{var decoded=Vencord.Plugins.plugins.ReactErrorDecoder.decodeError.apply(null, arguments);if(decoded)return decoded;${original}}`,
`${func}{var decoded=$self.decodeError.apply(null, arguments);if(decoded)return decoded;${original}}`,
},
},
],
async start() {
const CODES_URL = `https://raw.githubusercontent.com/facebook/react/v${React.version}/scripts/error-codes/codes.json`;
ERROR_CODES = await fetch(CODES_URL)
.then(res => res.json())
.catch(e => console.error("[ReactErrorDecoder] Failed to fetch React error codes\n", e));

View file

@ -42,6 +42,13 @@ export default definePlugin({
match: /codeBlock:\{react\((\i),(\i),(\i)\)\{/,
replace: "$&return $self.renderHighlighter($1,$2,$3);"
}
},
{
find: ".PREVIEW_NUM_LINES",
replacement: {
match: /(?<=function \i\((\i)\)\{)(?=let\{text:\i,language:)/,
replace: "return $self.renderHighlighter({lang:$1.language,content:$1.text});"
}
}
],
start: async () => {

View file

@ -77,7 +77,7 @@ export default definePlugin({
},
// Do not check for unreads when selecting the render level if the channel is hidden
{
match: /(?=!\(0,\i\.getHasImportantUnread\)\(this\.record\))/,
match: /(?<=&&)(?=!\i\.\i\.hasUnread\(this\.record\.id\))/,
replace: "$self.isHiddenChannel(this.record)||"
},
// Make channels we dont have access to be the same level as normal ones
@ -334,12 +334,12 @@ export default definePlugin({
replacement: [
{
// Remove the divider and the open chat button for the HiddenChannelLockScreen
match: /"more-options-popout"\)\),(?<=let{channel:(\i).+?inCall:(\i).+?)/,
match: /"more-options-popout"\)\),(?<=channel:(\i).+?inCall:(\i).+?)/,
replace: (m, channel, inCall) => `${m}${inCall}||!$self.isHiddenChannel(${channel},true)&&`
},
{
// Remove invite users button for the HiddenChannelLockScreen
match: /"popup".{0,100}?if\((?<=let{channel:(\i).+?inCall:(\i).+?)/,
match: /"popup".{0,100}?if\((?<=channel:(\i).+?inCall:(\i).+?)/,
replace: (m, channel, inCall) => `${m}(${inCall}||!$self.isHiddenChannel(${channel},true))&&`
},
]

View file

@ -80,16 +80,15 @@ function SilentMessageToggle(chatBoxProps: {
style={{ padding: "0 6px" }}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<g fill="currentColor">
<path d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4C13 3.69264 13.0198 3.3899 13.0582 3.093C12.7147 3.03189 12.3611 3 12 3C8.686 3 6 5.686 6 9V14C6 15.657 4.656 17 3 17V18H21V17C19.344 17 18 15.657 18 14V10.7101ZM8.55493 19C9.24793 20.19 10.5239 21 11.9999 21C13.4759 21 14.7519 20.19 15.4449 19H8.55493Z" />
<path d="M18.2624 5.50209L21 2.5V1H16.0349V2.49791H18.476L16 5.61088V7H21V5.50209H18.2624Z" />
{!enabled && <line x1="22" y1="2" x2="2" y2="22" stroke="var(--red-500)" stroke-width="2.5" />}
</g>
<svg width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" mask="url(#_)" d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4c0-.30736.0198-.6101.0582-.907C12.7147 3.03189 12.3611 3 12 3 8.686 3 6 5.686 6 9v5c0 1.657-1.344 3-3 3v1h18v-1c-1.656 0-3-1.343-3-3v-3.2899ZM8.55493 19c.693 1.19 1.96897 2 3.44497 2s2.752-.81 3.445-2H8.55493ZM18.2624 5.50209 21 2.5V1h-4.9651v1.49791h2.4411L16 5.61088V7h5V5.50209h-2.7376Z" />
{!enabled && <>
<mask id="_">
<path fill="#fff" d="M0 0h24v24H0Z" />
<path stroke="#000" stroke-width="5.99068" d="M0 24 24 0" />
</mask>
<path fill="var(--status-danger)" d="m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z" />
</>}
</svg>
</div>
</Button>

View file

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

View file

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

View file

@ -55,13 +55,19 @@ export default definePlugin({
replace: "return [$self.renderPlayer(),$1]"
}
},
// Adds POST and a Marker to the SpotifyAPI (so we can easily find it)
{
find: ".PLAYER_DEVICES",
replacement: {
replacement: [{
// Adds POST and a Marker to the SpotifyAPI (so we can easily find it)
match: /get:(\i)\.bind\(null,(\i\.\i)\.get\)/,
replace: "post:$1.bind(null,$2.post),$&"
}
},
{
// Spotify Connect API returns status 202 instead of 204 when skipping tracks.
// Discord rejects 202 which causes the request to send twice. This patch prevents this.
match: /202===\i\.status/,
replace: "false",
}]
},
// Discord doesn't give you the repeat kind, only a boolean
{

View file

@ -20,7 +20,7 @@ import { ApplicationCommandInputType, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { FluxDispatcher } from "@webpack/common";
import { FluxDispatcher, MessageActions } from "@webpack/common";
interface Album {
id: string;
@ -53,7 +53,6 @@ interface Track {
}
const Spotify = findByPropsLazy("getPlayerState");
const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage");
const PendingReplyStore = findByPropsLazy("getPendingReply");
function sendMessage(channelId, message) {
@ -65,7 +64,7 @@ function sendMessage(channelId, message) {
...message
};
const reply = PendingReplyStore.getPendingReply(channelId);
MessageCreator.sendMessage(channelId, message, void 0, MessageCreator.getSendMessageOptionsForReply(reply))
MessageActions.sendMessage(channelId, message, void 0, MessageActions.getSendMessageOptionsForReply(reply))
.then(() => {
if (reply) {
FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId });

View file

@ -46,10 +46,10 @@ export default definePlugin({
}
},
{
find: ".hasAvailableBurstCurrency)",
find: ".trackEmojiSearchEmpty,200",
replacement: {
match: /(?<=\.useBurstReactionsExperiment.{0,20})useState\(!1\)(?=.+?(\i===\i\.EmojiIntention.REACTION))/,
replace: "useState($self.settings.store.superReactByDefault && $1)"
match: /(\.trackEmojiSearchEmpty,200(?=.+?isBurstReaction:(\i).+?(\i===\i\.EmojiIntention.REACTION)).+?\[\2,\i\]=\i\.useState\().+?\)/,
replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.settings.store.superReactByDefault&&${isReactionIntention})`
}
}
],

View file

@ -19,19 +19,13 @@
import { definePluginSettings, Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { LazyComponent } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { find, findStoreLazy } from "@webpack";
import { ChannelStore, GuildMemberStore, i18n, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { findExportedComponentLazy, findStoreLazy } from "@webpack";
import { ChannelStore, GuildMemberStore, i18n, RelationshipStore, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { buildSeveralUsers } from "../typingTweaks";
const ThreeDots = LazyComponent(() => {
// This doesn't really need to explicitly find Dots' own module, but it's fine
const res = find(m => m.Dots && !m.Menu);
return res?.Dots;
});
const ThreeDots = findExportedComponentLazy("Dots", "AnimatedDots");
const TypingStore = findStoreLazy("TypingStore");
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
@ -53,7 +47,7 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
return oldKeys.length === currentKeys.length && currentKeys.every(key => old[key] != null);
}
);
const currentChannelId: string = useStateFromStores([SelectedChannelStore], () => SelectedChannelStore.getChannelId());
const guildId = ChannelStore.getChannel(channelId).guild_id;
if (!settings.store.includeMutedChannels) {
@ -61,6 +55,10 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
if (isChannelMuted) return null;
}
if (!settings.store.includeCurrentChannel) {
if (currentChannelId === channelId) return null;
}
const myId = UserStore.getCurrentUser()?.id;
const typingUsersArray = Object.keys(typingUsers).filter(id => id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers));
@ -107,6 +105,11 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
}
const settings = definePluginSettings({
includeCurrentChannel: {
type: OptionType.BOOLEAN,
description: "Whether to show the typing indicator for the currently selected channel",
default: true
},
includeMutedChannels: {
type: OptionType.BOOLEAN,
description: "Whether to show the typing indicator for muted channels.",

View file

@ -22,16 +22,12 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { LazyComponent } from "@utils/react";
import definePlugin from "@utils/types";
import { filters, find } from "@webpack";
import { findExportedComponentLazy } from "@webpack";
import { Menu, Popout, useState } from "@webpack/common";
import type { ReactNode } from "react";
const HeaderBarIcon = LazyComponent(() => {
const filter = filters.byCode(".HEADER_BAR_BADGE");
return find(m => m.Icon && filter(m.Icon)).Icon;
});
const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider");
function VencordPopout(onClose: () => void) {
const pluginEntries = [] as ReactNode[];

View file

@ -25,7 +25,7 @@ interface VoiceMessageProps {
src: string;
waveform: string;
}
const VoiceMessage = findComponentByCodeLazy<VoiceMessageProps>("waveform:");
const VoiceMessage = findComponentByCodeLazy<VoiceMessageProps>("waveform:", "onVolumeChange");
export type VoicePreviewOptions = {
src?: string;

View file

@ -26,7 +26,7 @@ import { useAwaiter } from "@utils/react";
import definePlugin from "@utils/types";
import { chooseFile } from "@utils/web";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { Button, FluxDispatcher, Forms, lodash, Menu, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { Button, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { ComponentType } from "react";
import { VoiceRecorderDesktop } from "./DesktopRecorder";
@ -36,7 +36,6 @@ import { VoicePreview } from "./VoicePreview";
import { VoiceRecorderWeb } from "./WebRecorder";
const CloudUtils = findByPropsLazy("CloudUpload");
const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage");
const PendingReplyStore = findStoreLazy("PendingReplyStore");
const OptionClasses = findByPropsLazy("optionName", "optionIcon", "optionLabel");
@ -100,7 +99,7 @@ function sendAudio(blob: Blob, meta: AudioMetadata) {
waveform: meta.waveform,
duration_secs: meta.duration,
}],
message_reference: reply ? MessageCreator.getSendMessageOptionsForReply(reply)?.messageReference : null,
message_reference: reply ? MessageActions.getSendMessageOptionsForReply(reply)?.messageReference : null,
}
});
});

View file

@ -20,9 +20,11 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { saveFile } from "@utils/web";
import { findByProps } from "@webpack";
import { findByPropsLazy } from "@webpack";
import { Clipboard, ComponentDispatch } from "@webpack/common";
const ctxMenuCallbacks = findByPropsLazy("contextMenuCallbackNative");
async function fetchImage(url: string) {
const res = await fetch(url);
if (res.status !== 200) return;
@ -55,7 +57,6 @@ export default definePlugin({
start() {
if (settings.store.addBack) {
const ctxMenuCallbacks = findByProps("contextMenuCallbackNative");
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
this.changedListeners = true;
@ -64,7 +65,6 @@ export default definePlugin({
stop() {
if (this.changedListeners) {
const ctxMenuCallbacks = findByProps("contextMenuCallbackNative");
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
}
@ -118,11 +118,12 @@ export default definePlugin({
// Add back image context menu
{
find: 'navId:"image-context"',
all: true,
predicate: () => settings.store.addBack,
replacement: {
// return IS_DESKTOP ? React.createElement(Menu, ...)
match: /return \i\.\i\?/,
replace: "return true?"
match: /return \i\.\i(?=\?|&&)/,
replace: "return true"
}
},

View file

@ -0,0 +1,15 @@
# XSOverlay Notifier
Sends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR.
## Preview
![](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40)
![](https://github.com/Vendicated/Vencord/assets/24845294/f15eff61-2d52-4620-bcab-808ecb1606d2)
## Usage
- Enable this plugin
- Set plugin settings as desired
- Open XSOverlay
- get ping spammed

View file

@ -0,0 +1,288 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, PluginNative } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
const enum ChannelTypes {
DM = 1,
GROUP_DM = 3
}
interface Message {
guild_id: string,
attachments: MessageAttachment[],
author: User,
channel_id: string,
components: any[],
content: string,
edited_timestamp: string,
embeds: Embed[],
sticker_items?: Sticker[],
flags: number,
id: string,
member: GuildMember,
mention_everyone: boolean,
mention_roles: string[],
mentions: Mention[],
nonce: string,
pinned: false,
referenced_message: any,
timestamp: string,
tts: boolean,
type: number;
}
interface Mention {
avatar: string,
avatar_decoration_data: any,
discriminator: string,
global_name: string,
id: string,
public_flags: number,
username: string;
}
interface Sticker {
t: "Sticker";
description: string;
format_type: number;
guild_id: string;
id: string;
name: string;
tags: string;
type: number;
}
interface Call {
channel_id: string,
guild_id: string,
message_id: string,
region: string,
ringing: string[];
}
const MuteStore = findByPropsLazy("isSuppressEveryoneEnabled");
const XSLog = new Logger("XSOverlay");
const settings = definePluginSettings({
ignoreBots: {
type: OptionType.BOOLEAN,
description: "Ignore messages from bots",
default: false
},
pingColor: {
type: OptionType.STRING,
description: "User mention color",
default: "#7289da"
},
channelPingColor: {
type: OptionType.STRING,
description: "Channel mention color",
default: "#8a2be2"
},
soundPath: {
type: OptionType.STRING,
description: "Notification sound (default/warning/error)",
default: "default"
},
timeout: {
type: OptionType.NUMBER,
description: "Notif duration (secs)",
default: 1.0,
},
opacity: {
type: OptionType.SLIDER,
description: "Notif opacity",
default: 1,
markers: makeRange(0, 1, 0.1)
},
volume: {
type: OptionType.SLIDER,
description: "Volume",
default: 0.2,
markers: makeRange(0, 1, 0.1)
},
});
const Native = VencordNative.pluginHelpers.XsOverlay as PluginNative<typeof import("./native")>;
export default definePlugin({
name: "XSOverlay",
description: "Forwards discord notifications to XSOverlay, for easy viewing in VR",
authors: [Devs.Nyako],
tags: ["vr", "notify"],
settings,
flux: {
CALL_UPDATE({ call }: { call: Call; }) {
if (call?.ringing?.includes(UserStore.getCurrentUser().id)) {
const channel = ChannelStore.getChannel(call.channel_id);
sendOtherNotif("Incoming call", `${channel.name} is calling you...`);
}
},
MESSAGE_CREATE({ message, optimistic }: { message: Message; optimistic: boolean; }) {
// Apparently without this try/catch, discord's socket connection dies if any part of this errors
try {
if (optimistic) return;
const channel = ChannelStore.getChannel(message.channel_id);
if (!shouldNotify(message, channel)) return;
const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
let finalMsg = message.content;
let titleString = "";
if (channel.guild_id) {
const guild = GuildStore.getGuild(channel.guild_id);
titleString = `${message.author.username} (${guild.name}, #${channel.name})`;
}
switch (channel.type) {
case ChannelTypes.DM:
titleString = message.author.username.trim();
break;
case ChannelTypes.GROUP_DM:
const channelName = channel.name.trim() ?? channel.rawRecipients.map(e => e.username).join(", ");
titleString = `${message.author.username} (${channelName})`;
break;
}
if (message.referenced_message) {
titleString += " (reply)";
}
if (message.embeds.length > 0) {
finalMsg += " [embed] ";
if (message.content === "") {
finalMsg = "sent message embed(s)";
}
}
if (message.sticker_items) {
finalMsg += " [sticker] ";
if (message.content === "") {
finalMsg = "sent a sticker";
}
}
const images = message.attachments.filter(e =>
typeof e?.content_type === "string"
&& e?.content_type.startsWith("image")
);
images.forEach(img => {
finalMsg += ` [image: ${img.filename}] `;
});
message.attachments.filter(a => a && !a.content_type?.startsWith("image")).forEach(a => {
finalMsg += ` [attachment: ${a.filename}] `;
});
// make mentions readable
if (message.mentions.length > 0) {
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`);
}
if (message.mention_roles.length > 0) {
for (const roleId of message.mention_roles) {
const role = GuildStore.getGuild(channel.guild_id).roles[roleId];
if (!role) continue;
const roleColor = role.colorString ?? `#${pingColor}`;
finalMsg = finalMsg.replace(`<@&${roleId}>`, `<b><color=${roleColor}>@${role.name}</color></b>`);
}
}
// make emotes and channel mentions readable
const emoteMatches = finalMsg.match(new RegExp("(<a?:\\w+:\\d+>)", "g"));
const channelMatches = finalMsg.match(new RegExp("<(#\\d+)>", "g"));
if (emoteMatches) {
for (const eMatch of emoteMatches) {
finalMsg = finalMsg.replace(new RegExp(`${eMatch}`, "g"), `:${eMatch.split(":")[1]}:`);
}
}
if (channelMatches) {
for (const cMatch of channelMatches) {
let channelId = cMatch.split("<#")[1];
channelId = channelId.substring(0, channelId.length - 1);
finalMsg = finalMsg.replace(new RegExp(`${cMatch}`, "g"), `<b><color=#${channelPingColor}>#${ChannelStore.getChannel(channelId).name}</color></b>`);
}
}
sendMsgNotif(titleString, finalMsg, message);
} catch (err) {
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
}
}
}
});
function sendMsgNotif(titleString: string, content: string, message: Message) {
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
const msgData = {
messageType: 1,
index: 0,
timeout: settings.store.timeout,
height: calculateHeight(cleanMessage(content)),
opacity: settings.store.opacity,
volume: settings.store.volume,
audioPath: settings.store.soundPath,
title: titleString,
content: content,
useBase64Icon: true,
icon: result,
sourceApp: "Vencord"
};
Native.sendToOverlay(msgData);
});
}
function sendOtherNotif(content: string, titleString: string) {
const msgData = {
messageType: 1,
index: 0,
timeout: settings.store.timeout,
height: calculateHeight(cleanMessage(content)),
opacity: settings.store.opacity,
volume: settings.store.volume,
audioPath: settings.store.soundPath,
title: titleString,
content: content,
useBase64Icon: false,
icon: null,
sourceApp: "Vencord"
};
Native.sendToOverlay(msgData);
}
function shouldNotify(message: Message, channel: Channel) {
const currentUser = UserStore.getCurrentUser();
if (message.author.id === currentUser.id) return false;
if (message.author.bot && settings.store.ignoreBots) return false;
if (MuteStore.allowAllMessages(channel) || message.mention_everyone && !MuteStore.isSuppressEveryoneEnabled(message.guild_id)) return true;
return message.mentions.some(m => m.id === currentUser.id);
}
function calculateHeight(content: string) {
if (content.length <= 100) return 100;
if (content.length <= 200) return 150;
if (content.length <= 300) return 200;
return 250;
}
function cleanMessage(content: string) {
return content.replace(new RegExp("<[^>]*>", "g"), "");
}

View file

@ -0,0 +1,16 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { createSocket, Socket } from "dgram";
let xsoSocket: Socket;
export function sendToOverlay(_, data: any) {
data.icon = Buffer.from(data.icon).toString("base64");
const json = JSON.stringify(data);
xsoSocket ??= createSocket("udp4");
xsoSocket.send(json, 42069, "127.0.0.1");
}

View file

@ -19,8 +19,7 @@
import * as DataStore from "@api/DataStore";
import { showNotification } from "@api/Notifications";
import { Settings } from "@api/Settings";
import { findByProps } from "@webpack";
import { UserStore } from "@webpack/common";
import { OAuth2AuthorizeModal, UserStore } from "@webpack/common";
import { Logger } from "./Logger";
import { openModal } from "./modal";
@ -91,8 +90,6 @@ export async function authorizeCloud() {
return;
}
const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal");
openModal((props: any) => <OAuth2AuthorizeModal
{...props}
scopes={["identify"]}

View file

@ -78,8 +78,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Samu",
id: 702973430449832038n,
},
Animal: {
name: "Animal",
Nyako: {
name: "nyako",
id: 118437263754395652n
},
MaiKokain: {
@ -387,6 +387,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "ant0n",
id: 145224646868860928n
},
philipbry: {
name: "philipbry",
id: 554994003318276106n
},
Korbo: {
name: "Korbo",
id: 455856406420258827n
},
maisymoe: {
name: "maisy",
id: 257109471589957632n,
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly

View file

@ -17,14 +17,42 @@
*/
import { MessageObject } from "@api/MessageEvents";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { Guild, Message, User } from "discord-types/general";
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
const MessageActions = findByPropsLazy("editMessage", "sendMessage");
const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal");
/**
* Open the invite modal
* @param code The invite code
* @returns Whether the invite was accepted
*/
export async function openInviteModal(code: string) {
const { invite } = await InviteActions.resolveInvite(code, "Desktop Modal");
if (!invite) throw new Error("Invalid invite: " + code);
FluxDispatcher.dispatch({
type: "INVITE_MODAL_OPEN",
invite,
code,
context: "APP"
});
return new Promise<boolean>(r => {
let onClose: () => void, onAccept: () => void;
let inviteAccepted = false;
FluxDispatcher.subscribe("INVITE_ACCEPT", onAccept = () => {
inviteAccepted = true;
});
FluxDispatcher.subscribe("INVITE_MODAL_CLOSE", onClose = () => {
FluxDispatcher.unsubscribe("INVITE_MODAL_CLOSE", onClose);
FluxDispatcher.unsubscribe("INVITE_ACCEPT", onAccept);
r(inviteAccepted);
});
});
}
export function getCurrentChannel() {
return ChannelStore.getChannel(SelectedChannelStore.getChannelId());

View file

@ -76,7 +76,7 @@ handler.getOwnPropertyDescriptor = (target, p) => {
};
/**
* Wraps the result of {@see makeLazy} in a Proxy you can consume as if it wasn't lazy.
* Wraps the result of {@link makeLazy} in a Proxy you can consume as if it wasn't lazy.
* On first property access, the lazy is evaluated
* @param factory lazy factory
* @param attempts how many times to try to evaluate the lazy before giving up

View file

@ -4,6 +4,8 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ComponentType } from "react";
import { makeLazy } from "./lazy";
const NoopComponent = () => null;
@ -16,8 +18,12 @@ const NoopComponent = () => null;
*/
export function LazyComponent<T extends object = any>(factory: () => React.ComponentType<T>, attempts = 5) {
const get = makeLazy(factory, attempts);
return (props: T) => {
const LazyComponent = (props: T) => {
const Component = get() ?? NoopComponent;
return <Component {...props} />;
};
LazyComponent.$$vencordInternal = get;
return LazyComponent as ComponentType<T>;
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { findByProps, findByPropsLazy } from "@webpack";
import { findByPropsLazy, findExportedComponentLazy } from "@webpack";
import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react";
import { LazyComponent } from "./react";
@ -118,7 +118,7 @@ export type ImageModal = ComponentType<{
shouldHideMediaOptions?: boolean;
}>;
export const ImageModal = LazyComponent(() => findByProps("ImageModal").ImageModal as ImageModal);
export const ImageModal = findExportedComponentLazy("ImageModal") as ImageModal;
export const ModalRoot = LazyComponent(() => Modals.ModalRoot);
export const ModalHeader = LazyComponent(() => Modals.ModalHeader);

View file

@ -17,7 +17,7 @@
*/
// eslint-disable-next-line path-alias/no-relative
import { filters, waitFor } from "@webpack";
import { filters, findByPropsLazy, waitFor } from "@webpack";
import { waitForComponent } from "./internal";
import * as t from "./types/components";
@ -55,6 +55,8 @@ export const MaskedLink = waitForComponent<t.MaskedLink>("MaskedLink", m => m?.t
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format"));
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
waitFor(["FormItem", "Button"], m => {
({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar } = m);
Forms = m;

View file

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

View file

@ -126,6 +126,7 @@ export type Button = ComponentType<PropsWithChildren<Omit<HTMLProps<HTMLButtonEl
buttonRef?: Ref<HTMLButtonElement>;
focusProps?: any;
submitting?: boolean;
submittingStartedLabel?: string;
submittingFinishedLabel?: string;

View file

@ -16,14 +16,23 @@
* 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";
// eslint-disable-next-line path-alias/no-relative
import { _resolveReady, find, findByPropsLazy, findLazy, waitFor } from "../webpack";
import { _resolveReady, filters, findByCodeLazy, findByPropsLazy, findLazy, waitFor } from "../webpack";
import type * as t from "./types/utils";
export let FluxDispatcher: t.FluxDispatcher;
waitFor(["dispatch", "subscribe"], m => {
FluxDispatcher = m;
const cb = () => {
m.unsubscribe("CONNECTION_OPEN", cb);
_resolveReady();
};
m.subscribe("CONNECTION_OPEN", cb);
});
export let ComponentDispatch;
waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch);
@ -41,7 +50,9 @@ export let SnowflakeUtils: t.SnowflakeUtils;
waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m);
export let Parser: t.Parser;
waitFor("parseTopic", m => Parser = m);
export let Alerts: t.Alerts;
waitFor(["show", "close"], m => Alerts = m);
const ToastType = {
MESSAGE: 0,
@ -82,6 +93,13 @@ export const Toasts = {
}
};
// This is the same module but this is easier
waitFor("showToast", m => {
Toasts.show = m.showToast;
Toasts.pop = m.popToast;
});
/**
* Show a simple toast. If you need more options, use Toasts.show manually
*/
@ -106,26 +124,16 @@ export const Clipboard: t.Clipboard = findByPropsLazy("SUPPORTS_COPY", "copy");
export const NavigationRouter: t.NavigationRouter = findByPropsLazy("transitionTo", "replaceWith", "transitionToGuild");
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;
waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m);
export const PermissionsBits: t.PermissionsBits = proxyLazy(() => find(m => typeof m.Permissions?.ADMINISTRATOR === "bigint").Permissions);
export const { Permissions: PermissionsBits } = findLazy(m => typeof m.Permissions?.ADMINISTRATOR === "bigint") as { Permissions: t.PermissionsBits; };
export const zustandCreate: typeof import("zustand").default = findByCodeLazy("will be removed in v4");
const persistFilter = filters.byCode("[zustand persist middleware]");
export const { persist: zustandPersist }: typeof import("zustand/middleware") = findLazy(m => m.persist && persistFilter(m.persist));
export const MessageActions = findByPropsLazy("editMessage", "sendMessage");
export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal");
export const InviteActions = findByPropsLazy("resolveInvite");

View file

@ -58,6 +58,9 @@ if (window[WEBPACK_CHUNK]) {
// normally, this is populated via webpackGlobal.push, which we patch below.
// However, Discord has their .m prepopulated.
// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
//
// Update: Discord now has TWO webpack instances. Their normal one and sentry
// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules
Object.defineProperty(Function.prototype, "m", {
set(v: any) {
// When using react devtools or other extensions, we may also catch their webpack here.
@ -65,8 +68,6 @@ if (window[WEBPACK_CHUNK]) {
if (new Error().stack?.includes("discord.com")) {
logger.info("Found webpack module factory");
patchFactories(v);
delete (Function.prototype as any).m;
}
Object.defineProperty(this, "m", {
@ -142,7 +143,7 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
// There are (at the time of writing) 11 modules exporting the window
// Make these non enumerable to improve webpack search performance
if (exports === window) {
if (exports === window && require.c) {
Object.defineProperty(require.c, id, {
value: require.c[id],
enumerable: false,
@ -152,11 +153,9 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
return;
}
const numberId = Number(id);
for (const callback of listeners) {
try {
callback(exports, numberId);
callback(exports, id);
} catch (err) {
logger.error("Error in webpack listener", err);
}
@ -166,10 +165,10 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
try {
if (filter(exports)) {
subscriptions.delete(filter);
callback(exports, numberId);
callback(exports, id);
} else if (exports.default && filter(exports.default)) {
subscriptions.delete(filter);
callback(exports.default, numberId);
callback(exports.default, id);
}
} catch (err) {
logger.error("Error while firing callback for webpack chunk", err);
@ -212,7 +211,7 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
}
if (patch.group) {
logger.warn(`Undoing patch ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
code = previousCode;
mod = previousMod;
patchedBy.delete(patch.plugin);
@ -260,7 +259,7 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
patchedBy.delete(patch.plugin);
if (patch.group) {
logger.warn(`Undoing patch ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
code = previousCode;
mod = previousMod;
break;

View file

@ -19,6 +19,7 @@
import { proxyLazy } from "@utils/lazy";
import { LazyComponent } from "@utils/lazyReact";
import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches";
import type { WebpackInstance } from "discord-types/other";
import { traceFunction } from "../debug/Tracer";
@ -69,7 +70,7 @@ export const filters = {
export const subscriptions = new Map<FilterFn, CallbackFn>();
export const listeners = new Set<CallbackFn>();
export type CallbackFn = (mod: any, id: number) => void;
export type CallbackFn = (mod: any, id: string) => void;
export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
if (cache !== void 0) throw "no.";
@ -111,12 +112,12 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn
if (!mod?.exports) continue;
if (filter(mod.exports)) {
return isWaitFor ? [mod.exports, Number(key)] : mod.exports;
return isWaitFor ? [mod.exports, key] : mod.exports;
}
if (mod.exports.default && filter(mod.exports.default)) {
const found = mod.exports.default;
return isWaitFor ? [found, Number(key)] : found;
return isWaitFor ? [found, key] : found;
}
}
@ -127,13 +128,6 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn
return isWaitFor ? [null, null] : null;
});
/**
* find but lazy
*/
export function findLazy(filter: FilterFn) {
return proxyLazy(() => find(filter));
}
export function findAll(filter: FilterFn) {
if (typeof filter !== "function")
throw new Error("Invalid filter. Expected a function got " + typeof filter);
@ -221,18 +215,21 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns
});
/**
* Find the id of a module by its code
* @param code Code
* @returns number or null
* Find the id of the first module factory that includes all the given code
* @returns string or null
*/
export const findModuleId = traceFunction("findModuleId", function findModuleId(code: string) {
export const findModuleId = traceFunction("findModuleId", function findModuleId(...code: string[]) {
outer:
for (const id in wreq.m) {
if (wreq.m[id].toString().includes(code)) {
return Number(id);
const str = wreq.m[id].toString();
for (const c of code) {
if (!str.includes(c)) continue outer;
}
return id;
}
const err = new Error("Didn't find module with code:\n" + code);
const err = new Error("Didn't find module with code(s):\n" + code.join("\n"));
if (IS_DEV) {
if (!devToolsOpen)
// Strict behaviour in DevBuilds to fail early and make sure the issue is found
@ -244,6 +241,60 @@ export const findModuleId = traceFunction("findModuleId", function findModuleId(
return null;
});
/**
* Find the first module factory that includes all the given code
* @returns The module factory or null
*/
export function findModuleFactory(...code: string[]) {
const id = findModuleId(...code);
if (!id) return null;
return wreq.m[id];
}
export const lazyWebpackSearchHistory = [] as Array<["find" | "findByProps" | "findByCode" | "findStore" | "findComponent" | "findComponentByCode" | "findExportedComponent" | "waitFor" | "waitForComponent" | "waitForStore" | "proxyLazyWebpack" | "LazyComponentWebpack" | "extractAndLoadChunks", any[]]>;
/**
* This is just a wrapper around {@link proxyLazy} to make our reporter test for your webpack finds.
*
* Wraps the result of {@link makeLazy} in a Proxy you can consume as if it wasn't lazy.
* On first property access, the lazy is evaluated
* @param factory lazy factory
* @param attempts how many times to try to evaluate the lazy before giving up
* @returns Proxy
*
* Note that the example below exists already as an api, see {@link findByPropsLazy}
* @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah);
*/
export function proxyLazyWebpack<T = any>(factory: () => any, attempts?: number) {
if (IS_DEV) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]);
return proxyLazy<T>(factory, attempts);
}
/**
* 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 LazyComponentWebpack<T extends object = any>(factory: () => any, attempts?: number) {
if (IS_DEV) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]);
return LazyComponent<T>(factory, attempts);
}
/**
* Find the first module that matches the filter, lazily
*/
export function findLazy(filter: FilterFn) {
if (IS_DEV) lazyWebpackSearchHistory.push(["find", [filter]]);
return proxyLazy(() => find(filter));
}
/**
* Find the first module that has the specified properties
*/
@ -255,14 +306,16 @@ export function findByProps(...props: string[]) {
}
/**
* findByProps but lazy
* Find the first module that has the specified properties, lazily
*/
export function findByPropsLazy(...props: string[]) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findByProps", props]);
return proxyLazy(() => findByProps(...props));
}
/**
* Find a function by its code
* Find the first function that includes all the given code
*/
export function findByCode(...code: string[]) {
const res = find(filters.byCode(...code), { isIndirect: true });
@ -272,9 +325,11 @@ export function findByCode(...code: string[]) {
}
/**
* findByCode but lazy
* Find the first function that includes all the given code, lazily
*/
export function findByCodeLazy(...code: string[]) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findByCode", code]);
return proxyLazy(() => findByCode(...code));
}
@ -289,9 +344,11 @@ export function findStore(name: string) {
}
/**
* findStore but lazy
* Find a store by its displayName, lazily
*/
export function findStoreLazy(name: string) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findStore", [name]]);
return proxyLazy(() => findStore(name));
}
@ -309,28 +366,108 @@ export function findComponentByCode(...code: string[]) {
* Finds the first component that matches the filter, lazily.
*/
export function findComponentLazy<T extends object = any>(filter: FilterFn) {
return LazyComponent<T>(() => find(filter));
if (IS_DEV) lazyWebpackSearchHistory.push(["findComponent", [filter]]);
return LazyComponent<T>(() => {
const res = find(filter, { isIndirect: true });
if (!res)
handleModuleNotFound("findComponent", filter);
return res;
});
}
/**
* Finds the first component that includes all the given code, lazily
*/
export function findComponentByCodeLazy<T extends object = any>(...code: string[]) {
return LazyComponent<T>(() => findComponentByCode(...code));
if (IS_DEV) lazyWebpackSearchHistory.push(["findComponentByCode", code]);
return LazyComponent<T>(() => {
const res = find(filters.componentByCode(...code), { isIndirect: true });
if (!res)
handleModuleNotFound("findComponentByCode", ...code);
return res;
});
}
/**
* Finds the first component that is exported by the first prop name, lazily
*/
export function findExportedComponentLazy<T extends object = any>(...props: string[]) {
return LazyComponent<T>(() => findByProps(...props)?.[props[0]]);
if (IS_DEV) lazyWebpackSearchHistory.push(["findExportedComponent", props]);
return LazyComponent<T>(() => {
const res = find(filters.byProps(...props), { isIndirect: true });
if (!res)
handleModuleNotFound("findExportedComponent", ...props);
return res[props[0]];
});
}
/**
* Extract and load chunks using their entry point
* @param code An array of all the code the module factory containing the entry point (as of using it to load chunks) must include
* @param matcher A RegExp that returns the entry point id as the first capture group. Defaults to a matcher that captures the first entry point found in the module factory
*/
export async function extractAndLoadChunks(code: string[], matcher: RegExp = /\.el\("(.+?)"\)(?<=(\i)\.el.+?)\.then\(\2\.bind\(\2,"\1"\)\)/) {
const module = findModuleFactory(...code);
if (!module) {
const err = new Error("extractAndLoadChunks: Couldn't find module factory");
logger.warn(err, "Code:", code, "Matcher:", matcher);
return;
}
const match = module.toString().match(canonicalizeMatch(matcher));
if (!match) {
const err = new Error("extractAndLoadChunks: Couldn't find entry point id in module factory code");
logger.warn(err, "Code:", code, "Matcher:", matcher);
// Strict behaviour in DevBuilds to fail early and make sure the issue is found
if (IS_DEV && !devToolsOpen)
throw err;
return;
}
const [, id] = match;
if (!id || !Number(id)) {
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the entry point, or the entry point returned wasn't a number");
logger.warn(err, "Code:", code, "Matcher:", matcher);
// Strict behaviour in DevBuilds to fail early and make sure the issue is found
if (IS_DEV && !devToolsOpen)
throw err;
return;
}
await (wreq as any).el(id);
return wreq(id as any);
}
/**
* This is just a wrapper around {@link extractAndLoadChunks} to make our reporter test for your webpack finds.
*
* Extract and load chunks using their entry point
* @param code An array of all the code the module factory containing the entry point (as of using it to load chunks) must include
* @param matcher A RegExp that returns the entry point id as the first capture group. Defaults to a matcher that captures the first entry point found in the module factory
* @returns A function that loads the chunks on first call
*/
export function extractAndLoadChunksLazy(code: string[], matcher: RegExp = /\.el\("(.+?)"\)(?<=(\i)\.el.+?)\.then\(\2\.bind\(\2,"\1"\)\)/) {
if (IS_DEV) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]);
return () => extractAndLoadChunks(code, matcher);
}
/**
* Wait for a module that matches the provided filter to be registered,
* then call the callback with the module as the first argument
*/
export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn) {
export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) {
if (IS_DEV && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]);
if (typeof filter === "string")
filter = filters.byProps(filter);
else if (Array.isArray(filter))
@ -383,7 +520,7 @@ export function search(...filters: Array<string | RegExp>) {
* so putting breakpoints or similar will have no effect.
* @param id The id of the module to extract
*/
export function extract(id: number) {
export function extract(id: string | number) {
const mod = wreq.m[id] as Function;
if (!mod) return null;

View file

@ -2,6 +2,7 @@
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"lib": [
"DOM",
"DOM.Iterable",