Compare commits
57 commits
09be5b9d06
...
beaf164aa8
Author | SHA1 | Date | |
---|---|---|---|
beaf164aa8 | |||
|
2cd82944e3 | ||
|
2f1dc2c704 | ||
|
a5442d87d5 | ||
|
a8b0ce6f03 | ||
|
b32959126e | ||
|
2cf52d0775 | ||
|
40b3ec57ce | ||
|
817cb9b60b | ||
|
c86de3299e | ||
|
1df0b571af | ||
|
6d911790e9 | ||
|
c9f7cf7540 | ||
|
a9568bc055 | ||
|
539e538d87 | ||
|
799e6e7292 | ||
|
510bfb8fa3 | ||
|
534ca1e28d | ||
|
d629281e72 | ||
|
34cbb22efe | ||
|
6ee50d30f6 | ||
|
fd9c675942 | ||
|
920252956f | ||
|
e4942397dc | ||
|
9faa1331da | ||
|
613b2dc5f6 | ||
|
9a89f7b3b2 | ||
|
c0b6d8f1c4 | ||
|
3453d0c97c | ||
|
cf7028331c | ||
|
08036f7af2 | ||
|
9dd00fb766 | ||
|
80016180b6 | ||
|
3e7d946296 | ||
|
fccdd3dc08 | ||
|
8e1546be00 | ||
|
66dbe7ef07 | ||
|
b47a5f569e | ||
|
8ef1882d43 | ||
|
9945219de7 | ||
|
091d29bf5e | ||
|
9b6308a835 | ||
|
597a74ff6c | ||
|
f814eeb74c | ||
|
1619ee404a | ||
|
1b179f3c6d | ||
|
6573c4757c | ||
|
ec16fd8741 | ||
|
6bbf562ab6 | ||
|
604cf00211 | ||
|
7c3b247d84 | ||
|
68fca78541 | ||
|
598ffe6368 | ||
|
534565db25 | ||
|
3e8e106be7 | ||
|
fdddfdb05b | ||
|
e14fba28a5 |
94 changed files with 3422 additions and 289 deletions
8
.github/workflows/reportBrokenPlugins.yml
vendored
8
.github/workflows/reportBrokenPlugins.yml
vendored
|
@ -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
|
||||
|
|
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
|
@ -1,11 +1,10 @@
|
|||
name: test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
43
src/plugins/badge.ts
Normal file
43
src/plugins/badge.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/* eslint-disable header/header */
|
||||
import { BadgePosition, ProfileBadge } from "@api/Badges";
|
||||
import { Badges } from "@api/index";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
const SHIGGY_BADGE = "https://cdn.discordapp.com/emojis/1101838344146665502.gif?size=240&quality=lossless";
|
||||
const BLOBFOXBOX_BADGE = "https://cdn.discordapp.com/emojis/1036216552736952350.webp?size=240&quality=lossless";
|
||||
|
||||
const ShiggyBadge: ProfileBadge = {
|
||||
description: "true shiggy fan",
|
||||
image: SHIGGY_BADGE,
|
||||
position: BadgePosition.START,
|
||||
props: {
|
||||
style: { transform: "scale(0.9)" }
|
||||
},
|
||||
shouldShow: ({ user }) => user.id === UserStore.getCurrentUser().id,
|
||||
link: "https://ryanccn.dev/"
|
||||
};
|
||||
const BlobfoxBoxBadge: ProfileBadge = {
|
||||
description: "blobfox",
|
||||
image: BLOBFOXBOX_BADGE,
|
||||
position: BadgePosition.START,
|
||||
props: {
|
||||
style: { transform: "scale(0.9)" }
|
||||
},
|
||||
shouldShow: ({ user }) => user.id === UserStore.getCurrentUser().id,
|
||||
link: "https://ryanccn.dev/"
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "Ryan's Extra Badges",
|
||||
description: "shiggy",
|
||||
authors: [Devs.RyanCaoDev],
|
||||
dependencies: ["BadgeAPI"],
|
||||
|
||||
|
||||
start() {
|
||||
Badges.addBadge(ShiggyBadge);
|
||||
Badges.addBadge(BlobfoxBoxBadge);
|
||||
},
|
||||
});
|
63
src/plugins/bottom/components/Indicator.tsx
Normal file
63
src/plugins/bottom/components/Indicator.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This plugin was modified from code licensed under the following license:
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021-present Sebastian Law
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Tooltip } from "@webpack/common";
|
||||
|
||||
export default function Indicator({ layers, bottom }: { layers: number; bottom: boolean; }) {
|
||||
return (
|
||||
<Tooltip color="black" position="top" text={layers <= 1 ? "🥺" : `Decoded from ${layers} nested bottom messages`}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<span
|
||||
className={`power-bottom-indicator ${findByPropsLazy("edited").edited}`}
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{bottom ? "(bottom)" : "(original)"}
|
||||
</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
73
src/plugins/bottom/encoding.ts
Normal file
73
src/plugins/bottom/encoding.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file was adapted from https://github.com/bottom-software-foundation/bottom-js
|
||||
* Which is, hopefully, licensed under MIT.
|
||||
*/
|
||||
|
||||
const CHARACTER_VALUES: [number, string][] = [
|
||||
[200, "🫂"],
|
||||
[50, "💖"],
|
||||
[10, "✨"],
|
||||
[5, "🥺"],
|
||||
[1, ","],
|
||||
[0, "❤️"],
|
||||
];
|
||||
const SECTION_SEPERATOR = "👉👈";
|
||||
const FINAL_TERMINATOR = new RegExp(`(${SECTION_SEPERATOR})?$`);
|
||||
|
||||
function encodeChar(charValue: number): string {
|
||||
if (charValue === 0) return "";
|
||||
const [val, currentCase]: [number, string] =
|
||||
CHARACTER_VALUES.find(([val]) => charValue >= val) || CHARACTER_VALUES[-1];
|
||||
return `${currentCase}${encodeChar(charValue - val)}`;
|
||||
}
|
||||
|
||||
export function encode(value: string): string {
|
||||
return Array.from(new TextEncoder().encode(value))
|
||||
.map((v: number) => encodeChar(v) + SECTION_SEPERATOR)
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function decode(value: string): string {
|
||||
return new TextDecoder().decode(Uint8Array.from(
|
||||
value
|
||||
.trim()
|
||||
.replace(FINAL_TERMINATOR, "")
|
||||
.split(SECTION_SEPERATOR)
|
||||
.map(letters => {
|
||||
return Array.from(letters)
|
||||
.map(character => {
|
||||
const [value, emoji]: [number, string] = CHARACTER_VALUES.find(
|
||||
([_, em]) => em === character
|
||||
) || [-1, ""];
|
||||
if (!emoji) {
|
||||
throw new TypeError(`Invalid bottom text: '${character}'`);
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.reduce((p, c) => p + c);
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
export default {
|
||||
encode: encode,
|
||||
decode: decode
|
||||
};
|
158
src/plugins/bottom/handler.ts
Normal file
158
src/plugins/bottom/handler.ts
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This plugin was modified from code licensed under the following license:
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021-present Sebastian Law
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { FluxDispatcher, MessageStore } from "@webpack/common";
|
||||
import type { Message } from "discord-types/general";
|
||||
|
||||
import Bottom from "./encoding";
|
||||
|
||||
class BottomHandler {
|
||||
|
||||
cache: Record<string, Record<string, { originalContent: string; top?: boolean; layers?: number; }>>;
|
||||
re: RegExp;
|
||||
|
||||
constructor() {
|
||||
this.cache = {};
|
||||
this.re = /((?:((?:\uD83E\uDEC2)?(?:💖)*(?:✨)*(?:🥺)*(?:,)*(❤️)?)(?:👉👈|\u200b))+)/gm;
|
||||
}
|
||||
|
||||
isTranslated(message) {
|
||||
if (
|
||||
!this.cache[message.channel_id] ||
|
||||
!this.cache[message.channel_id][message.id]
|
||||
) { return false; }
|
||||
|
||||
return this.cache[message.channel_id][message.id].originalContent !== message.content;
|
||||
}
|
||||
|
||||
translate(text: string, notNested: boolean) {
|
||||
var original = text;
|
||||
var translated = text;
|
||||
var layers = 0;
|
||||
while (original.match(this.re)) {
|
||||
translated = original.replace(this.re, (str, p1, offset, s) => Bottom.decode(p1) || p1);
|
||||
|
||||
// the regex can sometimes pick up invalid bottom in which case we want to return to avoid an infinite loop
|
||||
if (translated === original || notNested) break;
|
||||
else {
|
||||
original = translated;
|
||||
layers++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
translated: translated,
|
||||
layers: layers,
|
||||
};
|
||||
}
|
||||
|
||||
translateMessage(message: Message, decodeLayers: boolean) {
|
||||
if (!message.content || message.content.length === 0) {
|
||||
return "";
|
||||
}
|
||||
// Build cache if it doesn't exist
|
||||
if (!this.cache[message.channel_id]) {
|
||||
this.cache[message.channel_id] = {};
|
||||
}
|
||||
if (!this.cache[message.channel_id][message.id]) {
|
||||
this.cache[message.channel_id][message.id] = {
|
||||
originalContent: message.content,
|
||||
};
|
||||
}
|
||||
|
||||
const cached = this.cache[message.channel_id][message.id];
|
||||
|
||||
if (this.isTranslated(message)) {
|
||||
// if we're reverting back to original, just set the content back to original
|
||||
message.content = cached.originalContent;
|
||||
this.updateMessage(message);
|
||||
} else {
|
||||
// the message hasn't been edited, let's try to decode it
|
||||
const { translated, layers } = this.translate(message.content, !decodeLayers);
|
||||
if (translated === message.content) {
|
||||
// we don't want to do anything if there is no bottom
|
||||
// since the translation fails, mark this message to not show the indicator
|
||||
cached.top = true;
|
||||
throw new Error("No Bottom detected 🥺");
|
||||
} else {
|
||||
// let the indicator show how many layers of decoding we did
|
||||
cached.layers = layers;
|
||||
message.content = translated;
|
||||
this.updateMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateMessage(message: Message) {
|
||||
console.log({
|
||||
bottomTranslation: true,
|
||||
type: "MESSAGE_UPDATE",
|
||||
message,
|
||||
});
|
||||
FluxDispatcher.dispatch({
|
||||
bottomTranslation: true,
|
||||
type: "MESSAGE_UPDATE",
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
for (const channelID in this.cache) {
|
||||
for (const messageID in this.cache[channelID]) {
|
||||
this.removeMessage(channelID, messageID);
|
||||
}
|
||||
}
|
||||
this.cache = {};
|
||||
}
|
||||
|
||||
removeMessage(channelID: string, messageID: string, reset = true) {
|
||||
const message = MessageStore.getMessage(channelID, messageID);
|
||||
if (reset) {
|
||||
message.content = this.cache[channelID][messageID].originalContent;
|
||||
this.updateMessage(message);
|
||||
}
|
||||
delete this.cache[channelID][messageID];
|
||||
}
|
||||
}
|
||||
|
||||
export default BottomHandler;
|
262
src/plugins/bottom/index.tsx
Normal file
262
src/plugins/bottom/index.tsx
Normal file
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This plugin was modified from code licensed under the following license:
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021-present Sebastian Law
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { findOption, RequiredMessageOption } from "@api/Commands";
|
||||
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
|
||||
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
|
||||
import { addButton, removeButton } from "@api/MessagePopover";
|
||||
import { definePluginSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ChannelStore, Toasts } from "@webpack/common";
|
||||
|
||||
import Indicator from "./components/Indicator";
|
||||
import Bottom from "./encoding";
|
||||
import BottomHandler from "./handler";
|
||||
|
||||
const Handler = new BottomHandler();
|
||||
|
||||
const settings = definePluginSettings({
|
||||
"decode-layers": {
|
||||
description: "Decode Layers",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
"auto-encode-send": {
|
||||
description: "Automatically encode outgoing messages",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
},
|
||||
"encode-send-type": {
|
||||
description: "Automatic Encode Behavior",
|
||||
type: OptionType.SELECT,
|
||||
options:
|
||||
[
|
||||
{
|
||||
label: "All",
|
||||
default: true,
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: "Inline (Greedy)",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: "Inline (Parsed)",
|
||||
value: 2,
|
||||
}
|
||||
],
|
||||
},
|
||||
"inline-bottom-prefix": {
|
||||
description: "Inline bottom prefix",
|
||||
type: OptionType.STRING,
|
||||
default: "👉",
|
||||
},
|
||||
"inline-bottom-suffix": {
|
||||
description: "Inline bottom suffix",
|
||||
type: OptionType.STRING,
|
||||
default: "👈",
|
||||
},
|
||||
});
|
||||
|
||||
const escapeRegex: (string: string) => string = string => string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
function count(string: string, subString: string): number {
|
||||
var n = 0;
|
||||
var pos = 0;
|
||||
const step = subString.length;
|
||||
|
||||
while (true) {
|
||||
pos = string.indexOf(subString, pos);
|
||||
if (pos >= 0) {
|
||||
n++;
|
||||
pos += step;
|
||||
} else break;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
function inlineEncode(p: string, s: string, text: string): string {
|
||||
var np = count(text, p);
|
||||
var ns = count(text, s);
|
||||
|
||||
if (np === 0 || ns === 0) return text;
|
||||
|
||||
var pl = p.length;
|
||||
var sl = s.length;
|
||||
const result: string[] = [];
|
||||
let idx = 0;
|
||||
|
||||
while (true) {
|
||||
var startIndex = text.indexOf(p, idx);
|
||||
|
||||
if (startIndex < 0) {
|
||||
result.push(text.slice(idx));
|
||||
break;
|
||||
}
|
||||
|
||||
var endIndex = text.indexOf(s, startIndex + pl);
|
||||
|
||||
if (endIndex < 0) {
|
||||
result.push(text.slice(idx));
|
||||
break;
|
||||
}
|
||||
|
||||
result.push(text.slice(idx, startIndex));
|
||||
startIndex += pl;
|
||||
result.push(Bottom.encode(text.slice(startIndex, endIndex)));
|
||||
endIndex += sl;
|
||||
idx = endIndex;
|
||||
}
|
||||
|
||||
return result.join("");
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "Bottom",
|
||||
description: "The Vencord plugin for bottom 🥺",
|
||||
authors: [
|
||||
{
|
||||
id: 1038096782963507210n,
|
||||
name: "skyevg",
|
||||
},
|
||||
],
|
||||
dependencies: ["MessagePopoverAPI", "CommandsAPI", "MessageEventsAPI", "MessageAccessoriesAPI"],
|
||||
|
||||
settings,
|
||||
|
||||
start() {
|
||||
addButton("bottom", msg => {
|
||||
return {
|
||||
label: "Translate Bottom",
|
||||
icon: () => (
|
||||
<svg x="0" y="0" aria-hidden="false" width="22" height="22" viewBox="0 0 36 36" fill="currentColor" className="icon">
|
||||
<circle fill="#FFCC4D" cx="18" cy="18" r="18" />
|
||||
<path fill="#65471B" d="M20.996 27c-.103 0-.206-.016-.309-.049-1.76-.571-3.615-.571-5.375 0-.524.169-1.089-.117-1.26-.642-.171-.525.117-1.089.643-1.26 2.162-.702 4.447-.702 6.609 0 .525.171.813.735.643 1.26-.137.421-.529.691-.951.691z" />
|
||||
<path fill="#FFF" d="M30.335 12.068c-.903 2.745-3.485 4.715-6.494 4.715-.144 0-.289-.005-.435-.014-1.477-.093-2.842-.655-3.95-1.584.036.495.076.997.136 1.54.152 1.388.884 2.482 2.116 3.163.82.454 1.8.688 2.813.752 1.734.109 3.57-.28 4.873-.909 1.377-.665 2.272-1.862 2.456-3.285.183-1.415-.354-2.924-1.515-4.378z" />
|
||||
<path fill="#65471B" d="M21.351 7.583c-1.297.55-1.947 2.301-1.977 5.289l.039.068c.897 1.319 2.373 2.224 4.088 2.332.114.007.228.011.341.011 2.634 0 4.849-1.937 5.253-4.524-.115-.105-.221-.212-.343-.316-3.715-3.17-6.467-3.257-7.401-2.86z" />
|
||||
<path fill="#F4900C" d="M23.841 16.783c3.009 0 5.591-1.97 6.494-4.715-.354-.443-.771-.88-1.241-1.309-.404 2.587-2.619 4.524-5.253 4.524-.113 0-.227-.004-.341-.011-1.715-.108-3.191-1.013-4.088-2.332l-.039-.068c-.007.701.021 1.473.083 2.313 1.108.929 2.473 1.491 3.95 1.584.146.01.291.014.435.014z" />
|
||||
<circle fill="#FFF" cx="21.413" cy="10.705" r="1.107" />
|
||||
<path fill="#FFF" d="M12.159 16.783c-3.009 0-5.591-1.97-6.494-4.715-1.161 1.454-1.697 2.963-1.515 4.377.185 1.423 1.079 2.621 2.456 3.285 1.303.629 3.138 1.018 4.873.909 1.013-.064 1.993-.297 2.813-.752 1.231-.681 1.963-1.775 2.116-3.163.06-.542.1-1.042.136-1.536-1.103.923-2.47 1.487-3.95 1.58-.146.011-.291.015-.435.015z" />
|
||||
<path fill="#65471B" d="M12.159 15.283c.113 0 .227-.004.341-.011 1.715-.108 3.191-1.013 4.088-2.332l.039-.068c-.031-2.988-.68-4.739-1.977-5.289-.934-.397-3.687-.31-7.401 2.859-.122.104-.227.211-.343.316.404 2.588 2.619 4.525 5.253 4.525z" />
|
||||
<path fill="#F4900C" d="M16.626 12.872l-.039.068c-.897 1.319-2.373 2.224-4.088 2.332-.114.007-.228.011-.341.011-2.634 0-4.849-1.937-5.253-4.524-.47.429-.887.866-1.241 1.309.903 2.745 3.485 4.715 6.494 4.715.144 0 .289-.005.435-.014 1.48-.093 2.847-.657 3.95-1.58.062-.841.091-1.614.083-2.317z" />
|
||||
<path fill="#FFF" d="M9.781 11.81c.61-.038 1.074-.564 1.035-1.174-.038-.61-.564-1.074-1.174-1.036-.61.038-1.074.564-1.036 1.174.039.61.565 1.074 1.175 1.036z" />
|
||||
</svg>
|
||||
),
|
||||
message: msg,
|
||||
channel: ChannelStore.getChannel(msg.channel_id),
|
||||
onClick: async () => {
|
||||
try {
|
||||
Handler.translateMessage(msg, settings.store["decode-layers"]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Toasts.show(
|
||||
{
|
||||
id: Toasts.genId(),
|
||||
message: e.message,
|
||||
type: Toasts.Type.MESSAGE
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
addAccessory("bottom", props => {
|
||||
try {
|
||||
if (!Handler.cache[props.message.channel_id][props.message.id].top) {
|
||||
try {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Indicator layers={Handler.cache[props.message.channel_id][props.message.id].layers ?? 0} bottom={!Handler.isTranslated(props.message)} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
});
|
||||
|
||||
this.preSend = addPreSendListener((_, msg) => {
|
||||
if (settings.store["auto-encode-send"]) {
|
||||
const sendType = settings.store["encode-send-type"];
|
||||
var { content } = msg;
|
||||
|
||||
switch (sendType) {
|
||||
case 0: // all
|
||||
content = Bottom.encode(content);
|
||||
break;
|
||||
case 1: // inline greedy
|
||||
var prefix = escapeRegex(settings.store["inline-bottom-prefix"]);
|
||||
var suffix = escapeRegex(settings.store["inline-bottom-suffix"]);
|
||||
var reg = new RegExp(`${prefix}(.+)${suffix}`, "gm");
|
||||
content = content.replace(reg, (str, p1, o, s) => Bottom.encode(p1));
|
||||
break;
|
||||
case 2: // inline parsed
|
||||
var prefix = settings.store["inline-bottom-prefix"];
|
||||
var suffix = settings.store["inline-bottom-prefix"];
|
||||
content = inlineEncode(prefix, suffix, content);
|
||||
break;
|
||||
}
|
||||
msg.content = content;
|
||||
}
|
||||
});
|
||||
},
|
||||
stop() {
|
||||
removeButton("bottom");
|
||||
removeAccessory("bottom");
|
||||
removePreSendListener(this.preSend);
|
||||
},
|
||||
|
||||
commands: [
|
||||
{
|
||||
name: "bottom",
|
||||
description: "Translate and send text as bottom 🥺",
|
||||
options: [RequiredMessageOption],
|
||||
execute: opts => ({
|
||||
content: Bottom.encode(findOption(opts, "message", "")),
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
|
@ -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.*",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
17
src/plugins/decor/README.md
Normal file
17
src/plugins/decor/README.md
Normal 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
168
src/plugins/decor/index.tsx
Normal 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)
|
||||
});
|
83
src/plugins/decor/lib/api.ts
Normal file
83
src/plugins/decor/lib/api.ts
Normal 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());
|
16
src/plugins/decor/lib/constants.ts
Normal file
16
src/plugins/decor/lib/constants.ts
Normal 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
|
102
src/plugins/decor/lib/stores/AuthorizationStore.tsx
Normal file
102
src/plugins/decor/lib/stores/AuthorizationStore.tsx
Normal 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()
|
||||
}
|
||||
)
|
||||
));
|
56
src/plugins/decor/lib/stores/CurrentUserDecorationsStore.ts
Normal file
56
src/plugins/decor/lib/stores/CurrentUserDecorationsStore.ts
Normal 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 })
|
||||
})));
|
118
src/plugins/decor/lib/stores/UsersDecorationsStore.ts
Normal file
118
src/plugins/decor/lib/stores/UsersDecorationsStore.ts
Normal 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;
|
||||
}
|
17
src/plugins/decor/lib/utils/decoration.ts
Normal file
17
src/plugins/decor/lib/utils/decoration.ts
Normal 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 };
|
||||
}
|
|
@ -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)}
|
||||
/>;
|
||||
}
|
59
src/plugins/decor/ui/components/DecorSection.tsx
Normal file
59
src/plugins/decor/ui/components/DecorSection.tsx
Normal 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>;
|
||||
}
|
47
src/plugins/decor/ui/components/DecorationContextMenu.tsx
Normal file
47
src/plugins/decor/ui/components/DecorationContextMenu.tsx
Normal 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>;
|
||||
}
|
30
src/plugins/decor/ui/components/DecorationGridCreate.tsx
Normal file
30
src/plugins/decor/ui/components/DecorationGridCreate.tsx
Normal 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 >;
|
||||
}
|
30
src/plugins/decor/ui/components/DecorationGridNone.tsx
Normal file
30
src/plugins/decor/ui/components/DecorationGridNone.tsx
Normal 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 >;
|
||||
}
|
28
src/plugins/decor/ui/components/Grid.tsx
Normal file
28
src/plugins/decor/ui/components/Grid.tsx
Normal 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>;
|
||||
}
|
38
src/plugins/decor/ui/components/SectionedGridList.tsx
Normal file
38
src/plugins/decor/ui/components/SectionedGridList.tsx
Normal 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>;
|
||||
}
|
33
src/plugins/decor/ui/components/index.ts
Normal file
33
src/plugins/decor/ui/components/index.ts
Normal 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;
|
13
src/plugins/decor/ui/index.ts
Normal file
13
src/plugins/decor/ui/index.ts
Normal 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]:"]);
|
270
src/plugins/decor/ui/modals/ChangeDecorationModal.tsx
Normal file
270
src/plugins/decor/ui/modals/ChangeDecorationModal.tsx
Normal 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} />));
|
163
src/plugins/decor/ui/modals/CreateDecorationModal.tsx
Normal file
163
src/plugins/decor/ui/modals/CreateDecorationModal.tsx
Normal 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} />));
|
80
src/plugins/decor/ui/styles.css
Normal file
80
src/plugins/decor/ui/styles.css
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
25
src/plugins/fixImagesQuality/index.ts
Normal file
25
src/plugins/fixImagesQuality/index.ts
Normal 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: ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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[]) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
3
src/plugins/notificationVolume/README.md
Normal file
3
src/plugins/notificationVolume/README.md
Normal 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.
|
35
src/plugins/notificationVolume/index.ts
Normal file
35
src/plugins/notificationVolume/index.ts
Normal 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*"
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
|
||||
|
|
|
@ -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"]);
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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))&&`
|
||||
},
|
||||
]
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 — {since.toDateString()}</span>}
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
|
||||
options: {
|
||||
showDates: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show dates on friend requests",
|
||||
default: false,
|
||||
restartNeeded: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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})`
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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.",
|
||||
|
|
46
src/plugins/useAltSearch.ts
Normal file
46
src/plugins/useAltSearch.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "UseAlternativeSearch",
|
||||
description: "Use alternative search engine in right click menu",
|
||||
authors: [
|
||||
{
|
||||
id: 1038096782963507210n,
|
||||
name: "skyevg",
|
||||
},
|
||||
],
|
||||
patches: [
|
||||
{
|
||||
find: "https://www.google.com/search?q=",
|
||||
replacement: {
|
||||
match: /"https:\/\/www.google.com\/search\?q=".concat\(encodeURIComponent\(e\)\)/,
|
||||
replace: "Vencord.Settings.plugins.UseAlternativeSearch.source.replace(\"!QUERY!\", encodeURIComponent(e))"
|
||||
}
|
||||
}
|
||||
],
|
||||
options: {
|
||||
source: {
|
||||
description: "Search engine's url (use !QUERY! as replacement for the search term)",
|
||||
type: OptionType.STRING,
|
||||
default: "https://duckduckgo.com/?q=!QUERY!",
|
||||
}
|
||||
}
|
||||
});
|
143
src/plugins/uwuifier.ts
Normal file
143
src/plugins/uwuifier.ts
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { findOption, RequiredMessageOption } from "@api/Commands";
|
||||
import { addPreEditListener, addPreSendListener, MessageObject, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
const endings = [
|
||||
"rawr x3",
|
||||
"OwO",
|
||||
"UwU",
|
||||
"o.O",
|
||||
"-.-",
|
||||
">w<",
|
||||
"(⑅˘꒳˘)",
|
||||
"(ꈍᴗꈍ)",
|
||||
"(˘ω˘)",
|
||||
"(U ᵕ U❁)",
|
||||
"σωσ",
|
||||
"òωó",
|
||||
"(///ˬ///✿)",
|
||||
"(U ﹏ U)",
|
||||
"( ͡o ω ͡o )",
|
||||
"ʘwʘ",
|
||||
":3",
|
||||
":3", // important enough to have twice
|
||||
"XD",
|
||||
"nyaa~~",
|
||||
"mya",
|
||||
">_<",
|
||||
"😳",
|
||||
"🥺",
|
||||
"😳😳😳",
|
||||
"rawr",
|
||||
"^^",
|
||||
"^^;;",
|
||||
"(ˆ ﻌ ˆ)♡",
|
||||
"^•ﻌ•^",
|
||||
"/(^•ω•^)",
|
||||
"(✿oωo)"
|
||||
];
|
||||
|
||||
const replacements = [
|
||||
["small", "smol"],
|
||||
["cute", "kawaii~"],
|
||||
["fluff", "floof"],
|
||||
["love", "luv"],
|
||||
["stupid", "baka"],
|
||||
["what", "nani"],
|
||||
["meow", "nya~"],
|
||||
["hello", "hewwo"],
|
||||
];
|
||||
|
||||
const settings = definePluginSettings({
|
||||
uwuEveryMessage: {
|
||||
description: "Make every single message uwuified",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
restartNeeded: false
|
||||
}
|
||||
});
|
||||
|
||||
function selectRandomElement(arr) {
|
||||
// generate a random index based on the length of the array
|
||||
const randomIndex = Math.floor(Math.random() * arr.length);
|
||||
|
||||
// return the element at the randomly generated index
|
||||
return arr[randomIndex];
|
||||
}
|
||||
|
||||
|
||||
function uwuify(message: string): string {
|
||||
message = message.toLowerCase();
|
||||
// words
|
||||
for (const pair of replacements) {
|
||||
message = message.replaceAll(pair[0], pair[1]);
|
||||
}
|
||||
message = message
|
||||
.replaceAll(/([ \t\n])n/g, "$1ny") // nyaify
|
||||
.replaceAll(/[lr]/g, "w") // [lr] > w
|
||||
.replaceAll(/([ \t\n])([a-z])/g, (_, p1, p2) => Math.random() < .5 ? `${p1}${p2}-${p2}` : `${p1}${p2}`) // stutter
|
||||
.replaceAll(/([^.,!][.,!])([ \t\n])/g, (_, p1, p2) => `${p1} ${selectRandomElement(endings)}${p2}`); // endings
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// actual command declaration
|
||||
export default definePlugin({
|
||||
name: "UwUifier",
|
||||
description: "Simply uwuify commands",
|
||||
authors: [Devs.echo, Devs.skyevg, Devs.PandaNinjas],
|
||||
dependencies: ["CommandsAPI", "MessageEventsAPI"],
|
||||
settings,
|
||||
|
||||
commands: [
|
||||
{
|
||||
name: "uwuify",
|
||||
description: "uwuifies your messages",
|
||||
options: [RequiredMessageOption],
|
||||
|
||||
execute: opts => ({
|
||||
content: uwuify(findOption(opts, "message", "")),
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
||||
onSend(msg: MessageObject) {
|
||||
// Only run when it's enabled
|
||||
if (settings.store.uwuEveryMessage) {
|
||||
msg.content = uwuify(msg.content);
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
this.preSend = addPreSendListener((_, msg) => this.onSend(msg));
|
||||
this.preEdit = addPreEditListener((_cid, _mid, msg) =>
|
||||
this.onSend(msg)
|
||||
);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removePreSendListener(this.preSend);
|
||||
removePreEditListener(this.preEdit);
|
||||
},
|
||||
});
|
|
@ -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[];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
|
|
15
src/plugins/xsOverlay.desktop/README.md
Normal file
15
src/plugins/xsOverlay.desktop/README.md
Normal 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
|
288
src/plugins/xsOverlay.desktop/index.ts
Normal file
288
src/plugins/xsOverlay.desktop/index.ts
Normal 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"), "");
|
||||
}
|
16
src/plugins/xsOverlay.desktop/native.ts
Normal file
16
src/plugins/xsOverlay.desktop/native.ts
Normal 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");
|
||||
}
|
|
@ -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"]}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
1
src/webpack/common/types/components.d.ts
vendored
1
src/webpack/common/types/components.d.ts
vendored
|
@ -126,6 +126,7 @@ export type Button = ComponentType<PropsWithChildren<Omit<HTMLProps<HTMLButtonEl
|
|||
|
||||
buttonRef?: Ref<HTMLButtonElement>;
|
||||
focusProps?: any;
|
||||
submitting?: boolean;
|
||||
|
||||
submittingStartedLabel?: string;
|
||||
submittingFinishedLabel?: string;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
|
|
Loading…
Reference in a new issue