Merge branch 'Vendicated:main' into main

This commit is contained in:
Manti 2022-12-30 18:08:42 +03:00 committed by GitHub
commit 6a582f1cc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1181 additions and 284 deletions

View file

@ -84,7 +84,9 @@
"no-extra-semi": "error", "no-extra-semi": "error",
"consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }], "consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }],
"dot-notation": "error", "dot-notation": "error",
"no-useless-escape": "error", "no-useless-escape": ["error", {
"extra": "i"
}],
"no-fallthrough": "error", "no-fallthrough": "error",
"for-direction": "error", "for-direction": "error",
"no-async-promise-executor": "error", "no-async-promise-executor": "error",

13
.github/FUNDING.yml vendored
View file

@ -1,13 +0,0 @@
# These are supported funding model platforms
github: Vendicated
patreon: Aliucord
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View file

@ -55,10 +55,29 @@ jobs:
run: | run: |
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Upload Devbuild - name: Upload Devbuild as release
run: | run: |
gh release upload devbuild --clobber dist/* gh release upload devbuild --clobber dist/*
gh release edit devbuild --title "DevBuild $RELEASE_TAG" gh release edit devbuild --title "DevBuild $RELEASE_TAG"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ env.release_tag }} RELEASE_TAG: ${{ env.release_tag }}
- name: Upload Devbuild to builds repo
run: |
git config --global user.name "$USERNAME"
git config --global user.email actions@github.com
git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git upload
cd upload
rm -rf *
cp -r ../dist/* .
git add -A
git commit -m "Builds for https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA"
git push --force https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git
env:
API_TOKEN: ${{ secrets.BUILDS_TOKEN }}
GLOBIGNORE: .git:.gitignore:README.md:LICENSE
GH_REPO: Vencord/builds
USERNAME: GitHub-Actions

View file

@ -41,3 +41,17 @@ jobs:
env: env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
- name: Create Report (Canary)
timeout-minutes: 10
if: success() || failure() # even run if previous one failed
run: |
export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser)
export USE_CANARY=true
esbuild test/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

View file

@ -23,7 +23,7 @@ If you're a power user who wants to contribute and make plugins or just want to
Or install the browser extension for Or install the browser extension for
- [![Chrome](https://img.shields.io/badge/chrome-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip) - [![Chrome](https://img.shields.io/badge/chrome-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip)
- [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js) - Please note that QuickCSS, shiki and other plugins making use of external resources will not work with the UserScript. - [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS, shiki and other plugins making use of external resources will not work with the UserScript.
You may also build them from source, to do that do the same steps as in the manual regular install method, You may also build them from source, to do that do the same steps as in the manual regular install method,

View file

@ -2,7 +2,18 @@ if (typeof browser === "undefined") {
var browser = chrome; var browser = chrome;
} }
var script = document.createElement("script"); const script = document.createElement("script");
script.src = browser.runtime.getURL("dist/Vencord.js"); script.src = browser.runtime.getURL("dist/Vencord.js");
// documentElement because we load before body/head are ready
document.documentElement.appendChild(script); const style = document.createElement("link");
style.type = "text/css";
style.rel = "stylesheet";
style.href = browser.runtime.getURL("dist/Vencord.css");
document.documentElement.append(script);
document.addEventListener(
"DOMContentLoaded",
() => document.documentElement.append(style),
{ once: true }
);

View file

@ -18,7 +18,7 @@
"js": ["content.js"] "js": ["content.js"]
} }
], ],
"web_accessible_resources": ["dist/Vencord.js"], "web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
"background": { "background": {
"scripts": ["background.js"] "scripts": ["background.js"]
} }

View file

@ -23,7 +23,7 @@
"web_accessible_resources": [ "web_accessible_resources": [
{ {
"resources": ["dist/Vencord.js"], "resources": ["dist/Vencord.js", "dist/Vencord.css"],
"matches": ["*://*.discord.com/*"] "matches": ["*://*.discord.com/*"]
} }
], ],

View file

@ -34,6 +34,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/diff": "^5.0.2", "@types/diff": "^5.0.2",
"@types/lodash": "^4.14.0",
"@types/node": "^18.11.9", "@types/node": "^18.11.9",
"@types/react": "^18.0.25", "@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9", "@types/react-dom": "^18.0.9",
@ -62,7 +63,19 @@
"packageManager": "pnpm@7.13.4", "packageManager": "pnpm@7.13.4",
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch" "eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
"eslint@8.28.0": "patches/eslint@8.28.0.patch"
},
"peerDependencyRules": {
"ignoreMissing": [
"eslint-plugin-import"
]
},
"allowedDeprecatedVersions": {
"source-map-resolve": "*",
"resolve-url": "*",
"source-map-url": "*",
"urix": "*"
} }
}, },
"webExt": { "webExt": {

View file

@ -0,0 +1,45 @@
diff --git a/lib/rules/no-useless-escape.js b/lib/rules/no-useless-escape.js
index 2046a148a17fd1d5f3a4bbc9f45f7700259d11fa..f4898c6b57355a4fd72c43a9f32bf1a36a6ccf4a 100644
--- a/lib/rules/no-useless-escape.js
+++ b/lib/rules/no-useless-escape.js
@@ -97,12 +97,30 @@ module.exports = {
escapeBackslash: "Replace the `\\` with `\\\\` to include the actual backslash character."
},
- schema: []
+ schema: [{
+ type: "object",
+ properties: {
+ extra: {
+ type: "string",
+ default: ""
+ },
+ extraCharClass: {
+ type: "string",
+ default: ""
+ },
+ },
+ additionalProperties: false
+ }]
},
create(context) {
+ const options = context.options[0] || {};
+ const { extra, extraCharClass } = options || ''
const sourceCode = context.getSourceCode();
+ const NON_CHARCLASS_ESCAPES = union(REGEX_NON_CHARCLASS_ESCAPES, new Set(extra))
+ const CHARCLASS_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set(extraCharClass))
+
/**
* Reports a node
* @param {ASTNode} node The node to report
@@ -238,7 +256,7 @@ module.exports = {
.filter(charInfo => charInfo.escaped)
// Filter out characters that are valid to escape, based on their position in the regular expression.
- .filter(charInfo => !(charInfo.inCharClass ? REGEX_GENERAL_ESCAPES : REGEX_NON_CHARCLASS_ESCAPES).has(charInfo.text))
+ .filter(charInfo => !(charInfo.inCharClass ? CHARCLASS_ESCAPES : NON_CHARCLASS_ESCAPES).has(charInfo.text))
// Report all the remaining characters.
.forEach(charInfo => report(node, charInfo.index, charInfo.text));

32
pnpm-lock.yaml generated
View file

@ -4,9 +4,13 @@ patchedDependencies:
eslint-plugin-path-alias@1.0.0: eslint-plugin-path-alias@1.0.0:
hash: m6sma4g6bh67km3q6igf6uxaja hash: m6sma4g6bh67km3q6igf6uxaja
path: patches/eslint-plugin-path-alias@1.0.0.patch path: patches/eslint-plugin-path-alias@1.0.0.patch
eslint@8.28.0:
hash: 7wc6icvgtg3uswirb5tpsbjnbe
path: patches/eslint@8.28.0.patch
specifiers: specifiers:
'@types/diff': ^5.0.2 '@types/diff': ^5.0.2
'@types/lodash': ^4.14.0
'@types/node': ^18.11.9 '@types/node': ^18.11.9
'@types/react': ^18.0.25 '@types/react': ^18.0.25
'@types/react-dom': ^18.0.9 '@types/react-dom': ^18.0.9
@ -38,6 +42,7 @@ dependencies:
devDependencies: devDependencies:
'@types/diff': 5.0.2 '@types/diff': 5.0.2
'@types/lodash': 4.14.189
'@types/node': 18.11.9 '@types/node': 18.11.9
'@types/react': 18.0.25 '@types/react': 18.0.25
'@types/react-dom': 18.0.9 '@types/react-dom': 18.0.9
@ -50,7 +55,7 @@ devDependencies:
diff: 5.1.0 diff: 5.1.0
discord-types: 1.3.26 discord-types: 1.3.26
esbuild: 0.15.16 esbuild: 0.15.16
eslint: 8.28.0 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe
eslint-import-resolver-alias: 1.1.2 eslint-import-resolver-alias: 1.1.2
eslint-plugin-header: 3.1.1_eslint@8.28.0 eslint-plugin-header: 3.1.1_eslint@8.28.0
eslint-plugin-path-alias: 1.0.0_m6sma4g6bh67km3q6igf6uxaja_eslint@8.28.0 eslint-plugin-path-alias: 1.0.0_m6sma4g6bh67km3q6igf6uxaja_eslint@8.28.0
@ -149,6 +154,10 @@ packages:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true dev: true
/@types/lodash/4.14.189:
resolution: {integrity: sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA==}
dev: true
/@types/node/18.11.9: /@types/node/18.11.9:
resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==} resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==}
dev: true dev: true
@ -216,7 +225,7 @@ packages:
'@typescript-eslint/type-utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a '@typescript-eslint/type-utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
'@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
debug: 4.3.4 debug: 4.3.4
eslint: 8.28.0 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe
ignore: 5.2.0 ignore: 5.2.0
natural-compare-lite: 1.4.0 natural-compare-lite: 1.4.0
regexpp: 3.2.0 regexpp: 3.2.0
@ -241,7 +250,7 @@ packages:
'@typescript-eslint/types': 5.45.0 '@typescript-eslint/types': 5.45.0
'@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3
debug: 4.3.4 debug: 4.3.4
eslint: 8.28.0 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe
typescript: 4.9.3 typescript: 4.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -268,7 +277,7 @@ packages:
'@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3
'@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
debug: 4.3.4 debug: 4.3.4
eslint: 8.28.0 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe
tsutils: 3.21.0_typescript@4.9.3 tsutils: 3.21.0_typescript@4.9.3
typescript: 4.9.3 typescript: 4.9.3
transitivePeerDependencies: transitivePeerDependencies:
@ -312,7 +321,7 @@ packages:
'@typescript-eslint/scope-manager': 5.45.0 '@typescript-eslint/scope-manager': 5.45.0
'@typescript-eslint/types': 5.45.0 '@typescript-eslint/types': 5.45.0
'@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3
eslint: 8.28.0 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe
eslint-scope: 5.1.1 eslint-scope: 5.1.1
eslint-utils: 3.0.0_eslint@8.28.0 eslint-utils: 3.0.0_eslint@8.28.0
semver: 7.3.7 semver: 7.3.7
@ -894,7 +903,7 @@ packages:
peerDependencies: peerDependencies:
eslint: '>=7.7.0' eslint: '>=7.7.0'
dependencies: dependencies:
eslint: 8.28.0 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe
dev: true dev: true
/eslint-plugin-path-alias/1.0.0_m6sma4g6bh67km3q6igf6uxaja_eslint@8.28.0: /eslint-plugin-path-alias/1.0.0_m6sma4g6bh67km3q6igf6uxaja_eslint@8.28.0:
@ -902,7 +911,7 @@ packages:
peerDependencies: peerDependencies:
eslint: ^7 eslint: ^7
dependencies: dependencies:
eslint: 8.28.0 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe
nanomatch: 1.2.13 nanomatch: 1.2.13
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -914,7 +923,7 @@ packages:
peerDependencies: peerDependencies:
eslint: '>=5.0.0' eslint: '>=5.0.0'
dependencies: dependencies:
eslint: 8.28.0 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe
dev: true dev: true
/eslint-plugin-unused-imports/2.0.0_5am2datodjm2qi4eijrjrnoz54: /eslint-plugin-unused-imports/2.0.0_5am2datodjm2qi4eijrjrnoz54:
@ -928,7 +937,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au '@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au
eslint: 8.28.0 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe
eslint-rule-composer: 0.3.0 eslint-rule-composer: 0.3.0
dev: true dev: true
@ -959,7 +968,7 @@ packages:
peerDependencies: peerDependencies:
eslint: '>=5' eslint: '>=5'
dependencies: dependencies:
eslint: 8.28.0 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe
eslint-visitor-keys: 2.1.0 eslint-visitor-keys: 2.1.0
dev: true dev: true
@ -973,7 +982,7 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true dev: true
/eslint/8.28.0: /eslint/8.28.0_7wc6icvgtg3uswirb5tpsbjnbe:
resolution: {integrity: sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==} resolution: {integrity: sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
hasBin: true hasBin: true
@ -1020,6 +1029,7 @@ packages:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
patched: true
/espree/9.4.0: /espree/9.4.0:
resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==} resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==}

74
scripts/build/buildWeb.mjs Executable file → Normal file
View file

@ -20,13 +20,13 @@
import esbuild from "esbuild"; import esbuild from "esbuild";
import { zip } from "fflate"; import { zip } from "fflate";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import { readFileSync } from "fs";
import { readFile } from "fs/promises"; import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
import { join, resolve } from "path"; import { join } from "path";
// wtf is this assert syntax // wtf is this assert syntax
import PackageJSON from "../../package.json" assert { type: "json" }; import PackageJSON from "../../package.json" assert { type: "json" };
import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, globPlugins, watch } from "./common.mjs"; import { commonOpts, globPlugins, watch } from "./common.mjs";
/** /**
* @type {esbuild.BuildOptions} * @type {esbuild.BuildOptions}
@ -39,9 +39,7 @@ const commonOptions = {
external: ["plugins", "git-hash"], external: ["plugins", "git-hash"],
plugins: [ plugins: [
globPlugins, globPlugins,
gitHashPlugin, ...commonOpts.plugins,
gitRemotePlugin,
fileIncludePlugin
], ],
target: ["esnext"], target: ["esnext"],
define: { define: {
@ -77,9 +75,13 @@ await Promise.all(
] ]
); );
/**
* @type {(target: string, files: string[], shouldZip: boolean) => Promise<void>}
*/
async function buildPluginZip(target, files, shouldZip) { async function buildPluginZip(target, files, shouldZip) {
const entries = { const entries = {
"dist/Vencord.js": readFileSync("dist/browser.js"), "dist/Vencord.js": await readFile("dist/browser.js"),
"dist/Vencord.css": await readFile("dist/browser.css"),
...Object.fromEntries(await Promise.all(files.map(async f => [ ...Object.fromEntries(await Promise.all(files.map(async f => [
(f.startsWith("manifest") ? "manifest.json" : f), (f.startsWith("manifest") ? "manifest.json" : f),
await readFile(join("browser", f)) await readFile(join("browser", f))
@ -87,29 +89,47 @@ async function buildPluginZip(target, files, shouldZip) {
}; };
if (shouldZip) { if (shouldZip) {
zip(entries, {}, (err, data) => { return new Promise((resolve, reject) => {
if (err) { zip(entries, {}, (err, data) => {
console.error(err); if (err) {
process.exitCode = 1; reject(err);
} else { } else {
writeFileSync("dist/" + target, data); const out = join("dist", target);
console.info("Extension written to dist/" + target); writeFile(out, data).then(() => {
} console.info("Extension written to " + out);
resolve();
}).catch(reject);
}
});
}); });
} else { } else {
if (existsSync(target)) await rm(target, { recursive: true, force: true });
rmSync(target, { recursive: true }); await Promise.all(Object.entries(entries).map(async ([file, content]) => {
for (const entry in entries) { const dest = join("dist", target, file);
const destination = "dist/" + target + "/" + entry; const parentDirectory = join(dest, "..");
const parentDirectory = resolve(destination, ".."); await mkdir(parentDirectory, { recursive: true });
mkdirSync(parentDirectory, { recursive: true }); await writeFile(dest, content);
writeFileSync(destination, entries[entry]); }));
}
console.info("Unpacked Extension written to dist/" + target); console.info("Unpacked Extension written to dist/" + target);
} }
} }
await buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true); const cssText = "`" + readFileSync("dist/Vencord.user.css", "utf-8").replaceAll("`", "\\`") + "`";
await buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true); const cssRuntime = `
await buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false); ;document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(
Object.assign(document.createElement("style"), {
textContent: ${cssText},
id: "vencord-css-core"
}),
{ once: true }
));
`;
await Promise.all([
appendFile("dist/Vencord.user.js", cssRuntime),
buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true),
buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true),
buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false),
]);

View file

@ -17,9 +17,9 @@
*/ */
import { exec, execSync } from "child_process"; import { exec, execSync } from "child_process";
import { existsSync } from "fs"; import { existsSync, readFileSync } from "fs";
import { readdir, readFile } from "fs/promises"; import { readdir, readFile } from "fs/promises";
import { join } from "path"; import { join, relative } from "path";
import { promisify } from "util"; import { promisify } from "util";
export const watch = process.argv.includes("--watch"); export const watch = process.argv.includes("--watch");
@ -35,7 +35,7 @@ export const banner = {
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294 // https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const makeAllPackagesExternalPlugin = { export const makeAllPackagesExternalPlugin = {
name: "make-all-packages-external", name: "make-all-packages-external",
@ -46,7 +46,7 @@ export const makeAllPackagesExternalPlugin = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const globPlugins = { export const globPlugins = {
name: "glob-plugins", name: "glob-plugins",
@ -87,7 +87,7 @@ export const globPlugins = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const gitHashPlugin = { export const gitHashPlugin = {
name: "git-hash-plugin", name: "git-hash-plugin",
@ -103,7 +103,7 @@ export const gitHashPlugin = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const gitRemotePlugin = { export const gitRemotePlugin = {
name: "git-remote-plugin", name: "git-remote-plugin",
@ -125,7 +125,7 @@ export const gitRemotePlugin = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const fileIncludePlugin = { export const fileIncludePlugin = {
name: "file-include-plugin", name: "file-include-plugin",
@ -147,6 +147,31 @@ export const fileIncludePlugin = {
} }
}; };
const styleModule = readFileSync("./scripts/build/module/style.js", "utf-8");
/**
* @type {import("esbuild").Plugin}
*/
export const stylePlugin = {
name: "style-plugin",
setup: ({ onResolve, onLoad }) => {
onResolve({ filter: /\.css\?managed$/, namespace: "file" }, ({ path, resolveDir }) => ({
path: relative(process.cwd(), join(resolveDir, path.replace("?managed", ""))),
namespace: "managed-style",
}));
onLoad({ filter: /\.css$/, namespace: "managed-style" }, async ({ path }) => {
const css = await readFile(path, "utf-8");
const name = relative(process.cwd(), path).replaceAll("\\", "/");
return {
loader: "js",
contents: styleModule
.replaceAll("STYLE_SOURCE", JSON.stringify(css))
.replaceAll("STYLE_NAME", JSON.stringify(name))
};
});
}
};
/** /**
* @type {import("esbuild").BuildOptions} * @type {import("esbuild").BuildOptions}
*/ */
@ -158,7 +183,7 @@ export const commonOpts = {
sourcemap: watch ? "inline" : "", sourcemap: watch ? "inline" : "",
legalComments: "linked", legalComments: "linked",
banner, banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin], plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote"], external: ["~plugins", "~git-hash", "~git-remote"],
inject: ["./scripts/build/inject/react.mjs"], inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement", jsxFactory: "VencordCreateElement",

View file

@ -16,6 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export const VencordFragment = Symbol.for("react.fragment"); export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment");
export let VencordCreateElement = export let VencordCreateElement =
(...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args); (...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);

View file

@ -0,0 +1,26 @@
/*
* 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/>.
*/
(window.VencordStyles ??= new Map()).set(STYLE_NAME, {
name: STYLE_NAME,
source: STYLE_SOURCE,
classNames: {},
dom: null,
});
export default STYLE_NAME;

View file

@ -18,7 +18,6 @@
export * as Api from "./api"; export * as Api from "./api";
export * as Plugins from "./plugins"; export * as Plugins from "./plugins";
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
export * as Util from "./utils"; export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss"; export * as QuickCss from "./utils/quickCss";
export * as Updater from "./utils/updater"; export * as Updater from "./utils/updater";

View file

@ -17,7 +17,7 @@
*/ */
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import { HTMLProps } from "react"; import { ComponentType, HTMLProps } from "react";
import Plugins from "~plugins"; import Plugins from "~plugins";
@ -27,20 +27,21 @@ export enum BadgePosition {
} }
export interface ProfileBadge { export interface ProfileBadge {
/** The tooltip to show on hover */ /** The tooltip to show on hover. Required for image badges */
tooltip: string; tooltip?: string;
/** Custom component for the badge (tooltip not included) */
component?: ComponentType<ProfileBadge & BadgeUserArgs>;
/** The custom image to use */ /** The custom image to use */
image?: string; image?: string;
/** Action to perform when you click the badge */ /** Action to perform when you click the badge */
onClick?(): void; onClick?(): void;
/** Should the user display this badge? */ /** Should the user display this badge? */
shouldShow?(userInfo: BadgeUserArgs): boolean; shouldShow?(userInfo: BadgeUserArgs): boolean;
/** Optional props (e.g. style) for the badge */ /** Optional props (e.g. style) for the badge, ignored for component badges */
props?: HTMLProps<HTMLImageElement>; props?: HTMLProps<HTMLImageElement>;
/** Insert at start or end? */ /** Insert at start or end? */
position?: BadgePosition; position?: BadgePosition;
/** The badge name to display, Discord uses this. Required for component badges */
/** The badge name to display. Discord uses this, but we don't. */
key?: string; key?: string;
} }
@ -70,8 +71,8 @@ export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) {
for (const badge of Badges) { for (const badge of Badges) {
if (!badge.shouldShow || badge.shouldShow(args)) { if (!badge.shouldShow || badge.shouldShow(args)) {
badge.position === BadgePosition.START badge.position === BadgePosition.START
? badgeArray.unshift(badge) ? badgeArray.unshift({ ...badge, ...args })
: badgeArray.push(badge); : badgeArray.push({ ...badge, ...args });
} }
} }
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id); (Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id);

View file

@ -0,0 +1,65 @@
/*
* 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 { Channel, User } from "discord-types/general/index.js";
interface DecoratorProps {
activities: any[];
canUseAvatarDecorations: boolean;
channel: Channel;
/**
* Only for DM members
*/
channelName?: string;
/**
* Only for server members
*/
currentUser?: User;
guildId?: string;
isMobile: boolean;
isOwner?: boolean;
isTyping: boolean;
selected: boolean;
status: string;
user: User;
[key: string]: any;
}
export type Decorator = (props: DecoratorProps) => JSX.Element | null;
type OnlyIn = "guilds" | "dms";
export const decorators = new Map<string, { decorator: Decorator, onlyIn?: OnlyIn; }>();
export function addDecorator(identifier: string, decorator: Decorator, onlyIn?: OnlyIn) {
decorators.set(identifier, { decorator, onlyIn });
}
export function removeDecorator(identifier: string) {
decorators.delete(identifier);
}
export function __addDecoratorsToList(props: DecoratorProps): (JSX.Element | null)[] {
const isInGuild = !!(props.guildId);
return [...decorators.values()].map(decoratorObj => {
const { decorator, onlyIn } = decoratorObj;
// this can most likely be done cleaner
if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) {
return decorator(props);
}
return null;
});
}

View file

@ -0,0 +1,63 @@
/*
* 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 { Channel, Message } from "discord-types/general/index.js";
interface DecorationProps {
author: {
/**
* Will be username if the user has no nickname
*/
nick: string;
iconRoleId: string;
guildMemberAvatar: string;
colorRoleName: string;
colorString: string;
};
channel: Channel;
compact: boolean;
decorations: {
/**
* Element for the [BOT] tag if there is one
*/
0: JSX.Element | null;
/**
* Other decorations (including ones added with this api)
*/
1: JSX.Element[];
};
message: Message;
[key: string]: any;
}
export type Decoration = (props: DecorationProps) => JSX.Element | null;
export const decorations = new Map<string, Decoration>();
export function addDecoration(identifier: string, decoration: Decoration) {
decorations.set(identifier, decoration);
}
export function removeDecoration(identifier: string) {
decorations.delete(identifier);
}
export function __addDecorationsToMessage(props: DecorationProps): (JSX.Element | null)[] {
return [...decorations.values()].map(decoration => {
return decoration(props);
});
}

162
src/api/Styles.ts Normal file
View file

@ -0,0 +1,162 @@
/*
* 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 type { MapValue } from "type-fest/source/entry";
export type Style = MapValue<typeof VencordStyles>;
export const styleMap = window.VencordStyles ??= new Map();
export function requireStyle(name: string) {
const style = styleMap.get(name);
if (!style) throw new Error(`Style "${name}" does not exist`);
return style;
}
/**
* A style's name can be obtained from importing a stylesheet with `?managed` at the end of the import
* @param name The name of the style
* @returns `false` if the style was already enabled, `true` otherwise
* @example
* import pluginStyle from "./plugin.css?managed";
*
* // Inside some plugin method like "start()" or "[option].onChange()"
* enableStyle(pluginStyle);
*/
export function enableStyle(name: string) {
const style = requireStyle(name);
if (style.dom?.isConnected)
return false;
if (!style.dom) {
style.dom = document.createElement("style");
style.dom.dataset.vencordName = style.name;
}
compileStyle(style);
document.head.appendChild(style.dom);
return true;
}
/**
* @param name The name of the style
* @returns `false` if the style was already disabled, `true` otherwise
* @see {@link enableStyle} for info on getting the name of an imported style
*/
export function disableStyle(name: string) {
const style = requireStyle(name);
if (!style.dom?.isConnected)
return false;
style.dom.remove();
style.dom = null;
return true;
}
/**
* @param name The name of the style
* @returns `true` in most cases, may return `false` in some edge cases
* @see {@link enableStyle} for info on getting the name of an imported style
*/
export const toggleStyle = (name: string) => isStyleEnabled(name) ? disableStyle(name) : enableStyle(name);
/**
* @param name The name of the style
* @returns Whether the style is enabled
* @see {@link enableStyle} for info on getting the name of an imported style
*/
export const isStyleEnabled = (name: string) => requireStyle(name).dom?.isConnected ?? false;
/**
* Sets the variables of a style
* ```ts
* // -- plugin.ts --
* import pluginStyle from "./plugin.css?managed";
* import { setStyleVars } from "@api/Styles";
* import { findByPropsLazy } from "@webpack";
* const classNames = findByPropsLazy("thin", "scrollerBase"); // { thin: "thin-31rlnD scrollerBase-_bVAAt", ... }
*
* // Inside some plugin method like "start()"
* setStyleClassNames(pluginStyle, classNames);
* enableStyle(pluginStyle);
* ```
* ```scss
* // -- plugin.css --
* .plugin-root [--thin]::-webkit-scrollbar { ... }
* ```
* ```scss
* // -- final stylesheet --
* .plugin-root .thin-31rlnD.scrollerBase-_bVAAt::-webkit-scrollbar { ... }
* ```
* @param name The name of the style
* @param classNames An object where the keys are the variable names and the values are the variable values
* @param recompile Whether to recompile the style after setting the variables, defaults to `true`
* @see {@link enableStyle} for info on getting the name of an imported style
*/
export const setStyleClassNames = (name: string, classNames: Record<string, string>, recompile = true) => {
const style = requireStyle(name);
style.classNames = classNames;
if (recompile && isStyleEnabled(style.name))
compileStyle(style);
};
/**
* Updates the stylesheet after doing the following to the sourcecode:
* - Interpolate style classnames
* @param style **_Must_ be a style with a DOM element**
* @see {@link setStyleClassNames} for more info on style classnames
*/
export const compileStyle = (style: Style) => {
if (!style.dom) throw new Error("Style has no DOM element");
style.dom.textContent = style.source
.replace(/\[--(\w+)\]/g, (match, name) => {
const className = style.classNames[name];
return className ? classNameToSelector(className) : match;
});
};
/**
* @param name The classname
* @param prefix A prefix to add each class, defaults to `""`
* @return A css selector for the classname
* @example
* classNameToSelector("foo bar") // => ".foo.bar"
*/
export const classNameToSelector = (name: string, prefix = "") => name.split(" ").map(n => `.${prefix}${n}`).join("");
type ClassNameFactoryArg = string | string[] | Record<string, unknown>;
/**
* @param prefix The prefix to add to each class, defaults to `""`
* @returns A classname generator function
* @example
* const cl = classNameFactory("plugin-");
*
* cl("base", ["item", "editable"], { selected: null, disabled: true })
* // => "plugin-base plugin-item plugin-editable plugin-disabled"
*/
export const classNameFactory = (prefix: string = "") => (...args: ClassNameFactoryArg[]) => {
const classNames = new Set<string>();
for (const arg of args) {
if (typeof arg === "string") classNames.add(arg);
else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name));
else if (typeof arg === "object") Object.entries(arg).forEach(([name, value]) => value && classNames.add(name));
}
return Array.from(classNames, name => prefix + name).join(" ");
};

View file

@ -19,11 +19,14 @@
import * as $Badges from "./Badges"; import * as $Badges from "./Badges";
import * as $Commands from "./Commands"; import * as $Commands from "./Commands";
import * as $DataStore from "./DataStore"; import * as $DataStore from "./DataStore";
import * as $MemberListDecorators from "./MemberListDecorators";
import * as $MessageAccessories from "./MessageAccessories"; import * as $MessageAccessories from "./MessageAccessories";
import * as $MessageDecorations from "./MessageDecorations";
import * as $MessageEventsAPI from "./MessageEvents"; import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover"; import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices"; import * as $Notices from "./Notices";
import * as $ServerList from "./ServerList"; import * as $ServerList from "./ServerList";
import * as $Styles from "./Styles";
/** /**
* An API allowing you to listen to Message Clicks or run your own logic * An API allowing you to listen to Message Clicks or run your own logic
@ -31,16 +34,16 @@ import * as $ServerList from "./ServerList";
* *
* If your plugin uses this, you must add MessageEventsAPI to its dependencies * If your plugin uses this, you must add MessageEventsAPI to its dependencies
*/ */
const MessageEvents = $MessageEventsAPI; export const MessageEvents = $MessageEventsAPI;
/** /**
* An API allowing you to create custom notices * An API allowing you to create custom notices
* (snackbars on the top, like the Update prompt) * (snackbars on the top, like the Update prompt)
*/ */
const Notices = $Notices; export const Notices = $Notices;
/** /**
* An API allowing you to register custom commands * An API allowing you to register custom commands
*/ */
const Commands = $Commands; export const Commands = $Commands;
/** /**
* A wrapper around IndexedDB. This can store arbitrarily * A wrapper around IndexedDB. This can store arbitrarily
* large data and supports a lot of datatypes (Blob, Map, ...). * large data and supports a lot of datatypes (Blob, Map, ...).
@ -55,22 +58,33 @@ const Commands = $Commands;
* This is actually just idb-keyval, so if you're familiar with that, you're golden! * This is actually just idb-keyval, so if you're familiar with that, you're golden!
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types} * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}
*/ */
const DataStore = $DataStore; export const DataStore = $DataStore;
/** /**
* An API allowing you to add custom components as message accessories * An API allowing you to add custom components as message accessories
*/ */
const MessageAccessories = $MessageAccessories; export const MessageAccessories = $MessageAccessories;
/** /**
* An API allowing you to add custom buttons in the message popover * An API allowing you to add custom buttons in the message popover
*/ */
const MessagePopover = $MessagePopover; export const MessagePopover = $MessagePopover;
/** /**
* An API allowing you to add badges to user profiles * An API allowing you to add badges to user profiles
*/ */
const Badges = $Badges; export const Badges = $Badges;
/** /**
* An API allowing you to add custom elements to the server list * An API allowing you to add custom elements to the server list
*/ */
const ServerList = $ServerList; export const ServerList = $ServerList;
/**
export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList }; * An API allowing you to add components as message accessories
*/
export const MessageDecorations = $MessageDecorations;
/**
* An API allowing you to add components to member list users, in both DM's and servers
*/
export const MemberListDecorators = $MemberListDecorators;
/**
* An API allowing you to dynamically load styles
* a
*/
export const Styles = $Styles;

View file

@ -29,7 +29,12 @@ const setCss = debounce((css: string) => {
}); });
export async function launchMonacoEditor() { export async function launchMonacoEditor() {
const win = open("about:blank", void 0, "popup,width=1000,height=1000")!; const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
const win = open("about:blank", "VencordQuickCss", features);
if (!win) {
alert("Failed to open QuickCSS popup. Make sure to allow popups!");
return;
}
win.setCss = setCss; win.setCss = setCss;
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS); win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
@ -41,4 +46,6 @@ export async function launchMonacoEditor() {
: "vs-dark"; : "vs-dark";
win.document.write(monacoHtml); win.document.write(monacoHtml);
window.__VENCORD_MONACO_WIN__ = new WeakRef(win);
} }

View file

@ -18,6 +18,7 @@
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
import { makeCodeblock } from "@utils/misc"; import { makeCodeblock } from "@utils/misc";
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
import { search } from "@webpack"; import { search } from "@webpack";
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common"; import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
@ -41,20 +42,29 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
setModule([keys[0], candidates[keys[0]]]); setModule([keys[0], candidates[keys[0]]]);
}); });
function ReplacementComponent({ module, match, replacement, setReplacementError }) { interface ReplacementComponentProps {
module: [id: number, factory: Function];
match: string | RegExp;
replacement: string | ReplaceFn;
setReplacementError(error: any): void;
}
function ReplacementComponent({ module, match, replacement, setReplacementError }: ReplacementComponentProps) {
const [id, fact] = module; const [id, fact] = module;
const [compileResult, setCompileResult] = React.useState<[boolean, string]>(); const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
const [patchedCode, matchResult, diff] = React.useMemo(() => { const [patchedCode, matchResult, diff] = React.useMemo(() => {
const src: string = fact.toString().replaceAll("\n", ""); const src: string = fact.toString().replaceAll("\n", "");
const canonicalMatch = canonicalizeMatch(match);
try { try {
var patched = src.replace(match, replacement); const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
var patched = src.replace(canonicalMatch, canonicalReplace as string);
setReplacementError(void 0); setReplacementError(void 0);
} catch (e) { } catch (e) {
setReplacementError((e as Error).message); setReplacementError((e as Error).message);
return ["", [], []]; return ["", [], []];
} }
const m = src.match(match); const m = src.match(canonicalMatch);
return [patched, m, makeDiff(src, patched, m)]; return [patched, m, makeDiff(src, patched, m)];
}, [id, match, replacement]); }, [id, match, replacement]);
@ -179,9 +189,10 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
{Object.entries({ {Object.entries({
"$$": "Insert a $", "$$": "Insert a $",
"$&": "Insert the entire match", "$&": "Insert the entire match",
"$`": "Insert the substring before the match", "$`\u200b": "Insert the substring before the match",
"$'": "Insert the substring after the match", "$'": "Insert the substring after the match",
"$n": "Insert the nth capturing group ($1, $2...)" "$n": "Insert the nth capturing group ($1, $2...)",
"$self": "Insert the plugin instance",
}).map(([placeholder, desc]) => ( }).map(([placeholder, desc]) => (
<Forms.FormText key={placeholder}> <Forms.FormText key={placeholder}>
{Parser.parse("`" + placeholder + "`")}: {desc} {Parser.parse("`" + placeholder + "`")}: {desc}
@ -206,7 +217,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
function PatchHelper() { function PatchHelper() {
const [find, setFind] = React.useState<string>(""); const [find, setFind] = React.useState<string>("");
const [match, setMatch] = React.useState<string>(""); const [match, setMatch] = React.useState<string>("");
const [replacement, setReplacement] = React.useState<string | Function>(""); const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
const [replacementError, setReplacementError] = React.useState<string>(); const [replacementError, setReplacementError] = React.useState<string>();

View file

@ -0,0 +1,30 @@
/*
* 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 { BadgeStyle } from "@components/PluginSettings/styles";
export function Badge({ text, color }): JSX.Element {
return (
<div style={{
backgroundColor: color,
justifySelf: "flex-end",
marginLeft: "auto",
...BadgeStyle
}}>{text}</div>
);
}

View file

@ -29,6 +29,7 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
onError(hasError: boolean): void; onError(hasError: boolean): void;
} }
export * from "./BadgeComponent";
export * from "./SettingBooleanComponent"; export * from "./SettingBooleanComponent";
export * from "./SettingCustomComponent"; export * from "./SettingCustomComponent";
export * from "./SettingNumericComponent"; export * from "./SettingNumericComponent";

View file

@ -16,15 +16,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as DataStore from "@api/DataStore";
import { showNotice } from "@api/Notices"; import { showNotice } from "@api/Notices";
import { Settings, useSettings } from "@api/settings"; import { Settings, useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { handleComponentFailed } from "@components/handleComponentFailed"; import { handleComponentFailed } from "@components/handleComponentFailed";
import { Badge } from "@components/PluginSettings/components";
import PluginModal from "@components/PluginSettings/PluginModal";
import * as styles from "@components/PluginSettings/styles";
import { ChangeList } from "@utils/ChangeList"; import { ChangeList } from "@utils/ChangeList";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { classes, LazyComponent } from "@utils/misc"; import { classes, LazyComponent, useAwaiter } from "@utils/misc";
import { openModalLazy } from "@utils/modal"; import { openModalLazy } from "@utils/modal";
import { Plugin } from "@utils/types"; import { Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack"; import { findByCode, findByPropsLazy } from "@webpack";
@ -33,8 +37,6 @@ import { Alerts, Button, Forms, Margins, Parser, React, Select, Switch, Text, Te
import Plugins from "~plugins"; import Plugins from "~plugins";
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins"; import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
import PluginModal from "./PluginModal";
import * as styles from "./styles";
const logger = new Logger("PluginSettings", "#a6d189"); const logger = new Logger("PluginSettings", "#a6d189");
@ -78,9 +80,10 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
plugin: Plugin; plugin: Plugin;
disabled: boolean; disabled: boolean;
onRestartNeeded(name: string): void; onRestartNeeded(name: string): void;
isNew?: boolean;
} }
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave }: PluginCardProps) { function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
const settings = useSettings(); const settings = useSettings();
const pluginSettings = settings.plugins[plugin.name]; const pluginSettings = settings.plugins[plugin.name];
@ -162,8 +165,15 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
</Text>} </Text>}
hideBorder={true} hideBorder={true}
> >
<Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center" }}> <Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center", gap: "8px" }}>
<Text variant="text-md/bold" style={{ flexGrow: "1" }}>{plugin.name}</Text> <Text
variant="text-md/bold"
style={{
display: "flex", width: "100%", alignItems: "center", flexGrow: "1", gap: "8px"
}}
>
{plugin.name}{(isNew) && <Badge text="NEW" color="#ED4245" />}
</Text>
<button role="switch" onClick={() => openModal()} style={styles.SettingsIcon} className="button-12Fmur"> <button role="switch" onClick={() => openModal()} style={styles.SettingsIcon} className="button-12Fmur">
{plugin.options {plugin.options
? <CogWheel ? <CogWheel
@ -243,6 +253,23 @@ export default ErrorBoundary.wrap(function Settings() {
); );
}; };
const [newPlugins] = useAwaiter(() => DataStore.get("Vencord_existingPlugins").then((cachedPlugins: Record<string, number> | undefined) => {
const now = Date.now() / 1000;
const existingTimestamps: Record<string, number> = {};
const sortedPluginNames = Object.values(sortedPlugins).map(plugin => plugin.name);
const newPlugins: string[] = [];
for (const { name: p } of sortedPlugins) {
const time = existingTimestamps[p] = cachedPlugins?.[p] ?? now;
if ((time + 60 * 60 * 24 * 2) > now) {
newPlugins.push(p);
}
}
DataStore.set("Vencord_existingPlugins", existingTimestamps);
return window._.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
}));
return ( return (
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}> <Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
@ -281,6 +308,7 @@ export default ErrorBoundary.wrap(function Settings() {
onRestartNeeded={name => changes.add(name)} onRestartNeeded={name => changes.add(name)}
disabled={plugin.required || !!dependency} disabled={plugin.required || !!dependency}
plugin={plugin} plugin={plugin}
isNew={newPlugins?.includes(plugin.name)}
key={plugin.name} key={plugin.name}
/>; />;
}) })

View file

@ -29,7 +29,7 @@ export const PluginsGridItem: React.CSSProperties = {
borderRadius: 3, borderRadius: 3,
cursor: "pointer", cursor: "pointer",
display: "block", display: "block",
height: "min-content", height: "100%",
padding: 10, padding: 10,
width: "100%", width: "100%",
}; };
@ -48,3 +48,14 @@ export const SettingsIcon: React.CSSProperties = {
background: "transparent", background: "transparent",
marginRight: 8 marginRight: 8
}; };
export const BadgeStyle: React.CSSProperties = {
padding: "0 6px",
fontFamily: "var(--font-display)",
fontWeight: "500",
borderRadius: "8px",
height: "16px",
fontSize: "12px",
lineHeight: "16px",
color: "var(--white-500)",
};

View file

@ -16,22 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./settingsStyles.css";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { findByCodeLazy } from "@webpack"; import { findByCodeLazy } from "@webpack";
import { Forms, Router, Text } from "@webpack/common"; import { Forms, Router, Text } from "@webpack/common";
import cssText from "~fileContent/settingsStyles.css";
import BackupRestoreTab from "./BackupRestoreTab"; import BackupRestoreTab from "./BackupRestoreTab";
import PluginsTab from "./PluginsTab"; import PluginsTab from "./PluginsTab";
import ThemesTab from "./ThemesTab"; import ThemesTab from "./ThemesTab";
import Updater from "./Updater"; import Updater from "./Updater";
import VencordSettings from "./VencordTab"; import VencordSettings from "./VencordTab";
const style = document.createElement("style");
style.textContent = cssText;
document.head.appendChild(style);
const st = (style: string) => `vcSettings${style}`; const st = (style: string) => `vcSettings${style}`;
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]'); const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');

8
src/globals.d.ts vendored
View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { LoDashStatic } from "lodash";
declare global { declare global {
/** /**
@ -37,6 +38,12 @@ declare global {
export var VencordNative: typeof import("./VencordNative").default; export var VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord"); export var Vencord: typeof import("./Vencord");
export var VencordStyles: Map<string, {
name: string;
source: string;
classNames: Record<string, string>;
dom: HTMLStyleElement | null;
}>;
export var appSettings: { export var appSettings: {
set(setting: string, v: any): void; set(setting: string, v: any): void;
}; };
@ -54,6 +61,7 @@ declare global {
push(chunk: any): any; push(chunk: any): any;
pop(): any; pop(): any;
}; };
_: LoDashStatic;
[k: string]: any; [k: string]: any;
} }
} }

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./legacy";
import "./updater"; import "./updater";
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";

31
src/ipcMain/legacy.ts Normal file
View file

@ -0,0 +1,31 @@
/*
* 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 IpcEvents from "@utils/IpcEvents";
import { ipcMain } from "electron";
import { writeFile } from "fs/promises";
import { join } from "path";
import { get } from "./simpleGet";
ipcMain.handleOnce(IpcEvents.DOWNLOAD_VENCORD_CSS, async () => {
const buf = await get("https://github.com/Vendicated/Vencord/releases/download/devbuild/renderer.css");
await writeFile(join(__dirname, "renderer.css"), buf);
return buf.toString("utf-8");
});

View file

@ -24,7 +24,7 @@ export async function calculateHashes() {
const hashes = {} as Record<string, string>; const hashes = {} as Record<string, string>;
await Promise.all( await Promise.all(
["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => { ["patcher.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => {
const fis = createReadStream(join(__dirname, file)); const fis = createReadStream(join(__dirname, file));
const hash = createHash("sha1", { encoding: "hex" }); const hash = createHash("sha1", { encoding: "hex" });
fis.once("end", () => { fis.once("end", () => {

View file

@ -69,7 +69,7 @@ async function fetchUpdates() {
return false; return false;
data.assets.forEach(({ name, browser_download_url }) => { data.assets.forEach(({ name, browser_download_url }) => {
if (["patcher.js", "preload.js", "renderer.js"].some(s => name.startsWith(s))) { if (["patcher.js", "preload.js", "renderer.js", "renderer.css"].some(s => name.startsWith(s))) {
PendingUpdates.push([name, browser_download_url]); PendingUpdates.push([name, browser_download_url]);
} }
}); });

6
src/modules.d.ts vendored
View file

@ -37,3 +37,9 @@ declare module "~fileContent/*" {
const content: string; const content: string;
export default content; export default content;
} }
declare module "*.css" { }
declare module "*.css?managed" {
const name: string;
export default name;
}

View file

@ -0,0 +1,35 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "DisableDMCallIdle",
description: "Disables automatically getting kicked from a DM voice call after 5 minutes.",
authors: [Devs.Nuckyz],
patches: [
{
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
replacement: {
match: /function (?<functionName>.{1,3})\(\){.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT.+?}}/,
replace: "function $<functionName>(){}",
},
},
],
});

View file

@ -66,11 +66,20 @@ export default definePlugin({
/* Patch the badge list component on user profiles */ /* Patch the badge list component on user profiles */
{ {
find: "Messages.PROFILE_USER_BADGES,role:", find: "Messages.PROFILE_USER_BADGES,role:",
replacement: { replacement: [
match: /src:(\w{1,3})\[(\w{1,3})\.key\],/, {
// <img src={badge.image ?? imageMap[badge.key]} {...badge.props} /> match: /src:(\w{1,3})\[(\w{1,3})\.key\],/,
replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,` // <img src={badge.image ?? imageMap[badge.key]} {...badge.props} />
} replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,`
},
{
match: /spacing:(\d{1,2}),children:(.{1,40}(.{1,2})\.jsx.+(.{1,2})\.onClick.+\)})},/,
// if the badge provides it's own component, render that instead of an image
// the badge also includes info about the user that has it (type BadgeUserArgs), which is why it's passed as props
replace: (_, s, origBadgeComponent, React, badge) =>
`spacing:${s},children:${badge}.component ? () => (0,${React}.jsx)(${badge}.component, { ...${badge} }) : ${origBadgeComponent}},`
}
]
} }
], ],

View file

@ -0,0 +1,42 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "MemberListDecoratorsAPI",
description: "API to add decorators to member list (both in servers and DMs)",
authors: [Devs.TheSun],
patches: [
{
find: "lostPermissionTooltipText,",
replacement: {
match: /Fragment,{children:\[(.{30,80})\]/,
replace: "Fragment,{children:Vencord.Api.MemberListDecorators.__addDecoratorsToList(this.props).concat($1)"
}
},
{
find: "PrivateChannel.renderAvatar",
replacement: {
match: /(subText:(.{1,2})\.renderSubtitle\(\).{1,50}decorators):(.{30,100}:null)/,
replace: "$1:Vencord.Api.MemberListDecorators.__addDecoratorsToList($2.props).concat($3)"
}
}
],
});

View file

@ -0,0 +1,35 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "MessageDecorationsAPI",
description: "API to add decorations to messages",
authors: [Devs.TheSun],
patches: [
{
find: ".withMentionPrefix",
replacement: {
match: /(\(\).roleDot.{10,50}{children:.{1,2})}\)/,
replace: "$1.concat(Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0]))})"
}
}
],
});

View file

@ -16,12 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { migratePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
migratePluginSettings("NoticesAPI", "NoticesApi");
export default definePlugin({ export default definePlugin({
name: "NoticesAPI", name: "NoticesAPI",
description: "Fixes notices being automatically dismissed", description: "Fixes notices being automatically dismissed",
@ -29,12 +26,12 @@ export default definePlugin({
required: true, required: true,
patches: [ patches: [
{ {
find: "updateNotice:", find: 'displayName="NoticeStore"',
replacement: [ replacement: [
{ {
match: /;(.{1,2}=null;)(?=.{0,50}updateNotice)/g, match: /;.{1,2}=null;.{0,70}getPremiumSubscription/g,
replace: replace:
";if(Vencord.Api.Notices.currentNotice)return !1;$1" ";if(Vencord.Api.Notices.currentNotice)return false$&"
}, },
{ {
match: /(?<=NOTICE_DISMISS:function.+?){(?=if\(null==(.+?)\))/, match: /(?<=NOTICE_DISMISS:function.+?){(?=if\(null==(.+?)\))/,

View file

@ -50,7 +50,7 @@ function getGuildCandidates(isAnimated: boolean) {
} }
async function doClone(guildId: string, id: string, name: string, isAnimated: boolean) { async function doClone(guildId: string, id: string, name: string, isAnimated: boolean) {
const data = await fetch(`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`) const data = await fetch(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${isAnimated ? "gif" : "png"}`)
.then(r => r.blob()); .then(r => r.blob());
const reader = new FileReader(); const reader = new FileReader();
@ -226,7 +226,7 @@ export default definePlugin({
<img <img
role="presentation" role="presentation"
aria-hidden aria-hidden
src={`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`} src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${isAnimated ? "gif" : "png"}`}
alt="" alt=""
height={24} height={24}
width={24} width={24}

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./messageLogger.css";
import { Settings } from "@api/settings"; import { Settings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
@ -42,51 +44,14 @@ export default definePlugin({
timestampModule: null as any, timestampModule: null as any,
moment: null as Function | null, moment: null as Function | null,
css: `
.messagelogger-red-overlay .messageLogger-deleted {
background-color: rgba(240, 71, 71, 0.15);
}
.messagelogger-red-text .messageLogger-deleted div {
color: #f04747;
}
.messageLogger-deleted [class^="buttons"] {
display: none;
}
.messageLogger-deleted-attachment {
filter: grayscale(1);
}
.messageLogger-deleted-attachment:hover {
filter: grayscale(0);
transition: 250ms filter linear;
}
.theme-dark .messageLogger-edited {
filter: brightness(80%);
}
.theme-light .messageLogger-edited {
opacity: 0.5;
}
`,
start() { start() {
this.moment = findByPropsLazy("relativeTimeRounding", "relativeTimeThreshold"); this.moment = findByPropsLazy("relativeTimeRounding", "relativeTimeThreshold");
this.timestampModule = findByPropsLazy("messageLogger_TimestampComponent"); this.timestampModule = findByPropsLazy("messageLogger_TimestampComponent");
const style = this.style = document.createElement("style");
style.textContent = this.css;
style.id = "MessageLogger-css";
document.head.appendChild(style);
addDeleteStyleClass(); addDeleteStyleClass();
}, },
stop() { stop() {
this.style?.remove();
document.querySelectorAll(".messageLogger-deleted").forEach(e => e.remove()); document.querySelectorAll(".messageLogger-deleted").forEach(e => e.remove());
document.querySelectorAll(".messageLogger-edited").forEach(e => e.remove()); document.querySelectorAll(".messageLogger-edited").forEach(e => e.remove());
document.body.classList.remove("messagelogger-red-overlay"); document.body.classList.remove("messagelogger-red-overlay");

View file

@ -0,0 +1,27 @@
.messagelogger-red-overlay .messageLogger-deleted {
background-color: rgba(240, 71, 71, 0.15);
}
.messagelogger-red-text .messageLogger-deleted div {
color: #f04747;
}
.messageLogger-deleted [class^="buttons"] {
display: none;
}
.messageLogger-deleted-attachment {
filter: grayscale(1);
}
.messageLogger-deleted-attachment:hover {
filter: grayscale(0);
transition: 250ms filter linear;
}
.theme-dark .messageLogger-edited {
filter: brightness(80%);
}
.theme-light .messageLogger-edited {
opacity: 0.5;
}

View file

@ -0,0 +1,38 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "NoScreensharePreview",
description: "Disables screenshare previews from being sent.",
authors: [Devs.Nuckyz],
patches: [
{
find: '("ApplicationStreamPreviewUploadManager")',
replacement: [
".\\.default\\.makeChunkedRequest\\(",
".{1,2}\\..\\.post\\({url:"
].map(match => ({
match: new RegExp(`return\\[(?<code>\\d),${match}.\\..{1,3}\\.STREAM_PREVIEW.+?}\\)\\];`),
replace: 'return[$<code>,Promise.resolve({body:"",status:204})];'
}))
},
],
});

View file

@ -16,22 +16,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addBadge, BadgePosition, ProfileBadge, removeBadge } from "@api/Badges";
import { addDecorator, removeDecorator } from "@api/MemberListDecorators";
import { addDecoration, removeDecoration } from "@api/MessageDecorations";
import { Settings } from "@api/settings"; import { Settings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { PresenceStore, Tooltip } from "@webpack/common"; import { PresenceStore, Tooltip, UserStore } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
const SessionStore = findByPropsLazy("getActiveSession");
function Icon(path: string, viewBox = "0 0 24 24") { function Icon(path: string, viewBox = "0 0 24 24") {
return ({ color, tooltip }: { color: string; tooltip: string; }) => ( return ({ color, tooltip }: { color: string; tooltip: string; }) => (
<Tooltip text={tooltip} > <Tooltip text={tooltip} >
{(tooltipProps: any) => ( {(tooltipProps: any) => (
<svg <svg
{...tooltipProps} {...tooltipProps}
height="18" height="20"
width="18" width="20"
viewBox={viewBox} viewBox={viewBox}
fill={color} fill={color}
> >
@ -59,9 +64,33 @@ const PlatformIcon = ({ platform, status }: { platform: Platform, status: string
return <Icon color={`var(--${getStatusColor(status)}`} tooltip={tooltip} />; return <Icon color={`var(--${getStatusColor(status)}`} tooltip={tooltip} />;
}; };
const PlatformIndicator = ({ user }: { user: User; }) => { const getStatus = (id: string): Record<Platform, string> => PresenceStore.getState()?.clientStatuses?.[id];
const PlatformIndicator = ({ user, inline = false, marginLeft = "4px" }: { user: User; inline?: boolean; marginLeft?: string; }) => {
if (!user || user.bot) return null; if (!user || user.bot) return null;
if (user.id === UserStore.getCurrentUser().id) {
const sessions = SessionStore.getSessions();
if (typeof sessions !== "object") return null;
const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => {
if (a === b) return 0;
if (a === "online") return 1;
if (b === "online") return -1;
if (a === "idle") return 1;
if (b === "idle") return -1;
return 0;
});
const ownStatus = Object.values(sortedSessions).reduce((acc: any, curr: any) => {
if (curr.clientInfo.client !== "unknown")
acc[curr.clientInfo.client] = curr.status;
return acc;
}, {});
const { clientStatuses } = PresenceStore.getState();
clientStatuses[UserStore.getCurrentUser().id] = ownStatus;
}
const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record<Platform, string>; const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record<Platform, string>;
if (!status) return null; if (!status) return null;
@ -79,7 +108,11 @@ const PlatformIndicator = ({ user }: { user: User; }) => {
<div <div
className="vc-platform-indicator" className="vc-platform-indicator"
style={{ style={{
display: "flex", alignItems: "center", marginLeft: "4px", gap: "4px" marginLeft,
gap: "4px",
display: inline ? "inline-flex" : "flex",
alignItems: "center",
transform: inline ? "translateY(4px)" : undefined
}} }}
> >
{icons} {icons}
@ -87,67 +120,84 @@ const PlatformIndicator = ({ user }: { user: User; }) => {
); );
}; };
const badge: ProfileBadge = {
component: p => <PlatformIndicator {...p} marginLeft="" />,
position: BadgePosition.START,
shouldShow: userInfo => !!Object.keys(getStatus(userInfo.user.id) ?? {}).length,
key: "indicator"
};
const indicatorLocations = {
list: {
description: "In the member list",
onEnable: () => addDecorator("platform-indicator", props =>
<ErrorBoundary noop>
<PlatformIndicator user={props.user} />
</ErrorBoundary>
),
onDisable: () => removeDecorator("platform-indicator")
},
badges: {
description: "In user profiles, as badges",
onEnable: () => addBadge(badge),
onDisable: () => removeBadge(badge)
},
messages: {
description: "Inside messages",
onEnable: () => addDecoration("platform-indicator", props =>
<ErrorBoundary noop>
<PlatformIndicator user={
props.decorations[1]?.find(i => i.key === "new-member")?.props.message?.author
} inline />
</ErrorBoundary>
),
onDisable: () => removeDecoration("platform-indicator")
}
};
export default definePlugin({ export default definePlugin({
name: "PlatformIndicators", name: "PlatformIndicators",
description: "Adds platform indicators (Desktop, Mobile, Web...) to users", description: "Adds platform indicators (Desktop, Mobile, Web...) to users",
authors: [Devs.kemo], authors: [Devs.kemo, Devs.TheSun],
dependencies: ["MessageDecorationsAPI", "MemberListDecoratorsAPI"],
patches: [ start() {
{ const settings = Settings.plugins.PlatformIndicators;
// Server member list decorators const { displayMode } = settings;
find: "this.renderPremium()",
predicate: () => ["both", "list"].includes(Settings.plugins.PlatformIndicators.displayMode), // transfer settings from the old ones, which had a select menu instead of booleans
replacement: { if (displayMode) {
match: /this.renderPremium\(\)[^\]]*?\]/, if (displayMode !== "both") settings[displayMode] = true;
replace: "$&.concat(Vencord.Plugins.plugins.PlatformIndicators.renderPlatformIndicators(this.props))" else {
} settings.list = true;
}, settings.badges = true;
{
// Dm list decorators
find: "PrivateChannel.renderAvatar",
predicate: () => ["both", "list"].includes(Settings.plugins.PlatformIndicators.displayMode),
replacement: {
match: /(subText:(.{1,3})\..+?decorators:)(.+?:null)/,
replace: "$1[$3].concat(Vencord.Plugins.plugins.PlatformIndicators.renderPlatformIndicators($2.props))"
}
},
{
// User badges
find: "Messages.PROFILE_USER_BADGES",
predicate: () => ["both", "badges"].includes(Settings.plugins.PlatformIndicators.displayMode),
replacement: {
match: /(Messages\.PROFILE_USER_BADGES,role:"group",children:)(.+?\.key\)\}\)\))/,
replace: "$1[Vencord.Plugins.plugins.PlatformIndicators.renderPlatformIndicators(e)].concat($2)"
} }
settings.messages = true;
delete settings.displayMode;
} }
],
renderPlatformIndicators: ({ user }: { user: User; }) => ( Object.entries(indicatorLocations).forEach(([key, value]) => {
<ErrorBoundary noop> if (settings[key]) value.onEnable();
<PlatformIndicator user={user} /> });
</ErrorBoundary> },
),
stop() {
Object.entries(indicatorLocations).forEach(([_, value]) => {
value.onDisable();
});
},
options: { options: {
displayMode: { ...Object.fromEntries(
type: OptionType.SELECT, Object.entries(indicatorLocations).map(([key, value]) => {
description: "Where to display the platform indicators", return [key, {
restartNeeded: true, type: OptionType.BOOLEAN,
options: [ description: `Show indicators ${value.description.toLowerCase()}`,
{ // onChange doesn't give any way to know which setting was changed, so restart required
label: "Member List & Badges", restartNeeded: true,
value: "both",
default: true default: true
}, }];
{ })
label: "Member List Only", )
value: "list"
},
{
label: "Badges Only",
value: "badges"
}
]
},
} }
}); });

View file

@ -43,17 +43,10 @@ export default definePlugin({
}, },
// Hijack the discord pronouns section (hidden without experiment) and add a wrapper around the text section // Hijack the discord pronouns section (hidden without experiment) and add a wrapper around the text section
{ {
find: "currentPronouns:", find: ".Messages.BOT_PROFILE_SLASH_COMMANDS",
all: true,
noWarn: true,
replacement: { replacement: {
match: /\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^[}]*currentPronouns:[^}]*(\w)\.pronouns[^}]*\})\)/, match: /\(0,.\.jsx\)\((?<PronounComponent>.{1,2}\..),(?<pronounProps>{currentPronouns.+?:(?<fullProps>.{1,2})\.pronouns.+?})\)/,
replace: (original, PronounComponent, pronounProps, fullProps) => { replace: "$<fullProps>&&Vencord.Plugins.plugins.PronounDB.PronounsProfileWrapper($<PronounComponent>,$<pronounProps>,$<fullProps>)"
// UserSettings
if (pronounProps.includes("onPronounsChange")) return original;
return `${fullProps}&&Vencord.Plugins.plugins.PronounDB.PronounsProfileWrapper(${PronounComponent}, ${pronounProps}, ${fullProps})`;
}
} }
}, },
// Make pronouns experiment be enabled by default // Make pronouns experiment be enabled by default

View file

@ -33,7 +33,7 @@ export function Header({ langName, useDevIcon, shikiLang }: HeaderProps) {
<div className={cl("lang")}> <div className={cl("lang")}>
{useDevIcon !== DeviconSetting.Disabled && shikiLang?.devicon && ( {useDevIcon !== DeviconSetting.Disabled && shikiLang?.devicon && (
<i <i
className={`devicon-${shikiLang.devicon}${useDevIcon === DeviconSetting.Color ? " colored" : ""}`} className={`${cl("devicon")} devicon-${shikiLang.devicon}${useDevIcon === DeviconSetting.Color ? " colored" : ""}`}
/> />
)} )}
{langName} {langName}

View file

@ -90,14 +90,10 @@ export const Highlighter = ({
let langName; let langName;
if (lang) langName = useHljs ? hljs?.getLanguage?.(lang)?.name : shikiLang?.name; if (lang) langName = useHljs ? hljs?.getLanguage?.(lang)?.name : shikiLang?.name;
const preClasses = [cl("root")];
if (!langName) preClasses.push(cl("plain"));
if (isPreview) preClasses.push(cl("preview"));
return ( return (
<div <div
ref={rootRef} ref={rootRef}
className={preClasses.join(" ")} className={cl("root", { plain: !langName, preview: isPreview })}
style={{ style={{
backgroundColor: useHljs backgroundColor: useHljs
? themeBase.backgroundColor ? themeBase.backgroundColor

View file

@ -0,0 +1 @@
@import url('https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css');

View file

@ -16,23 +16,25 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./shiki.css";
import { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { parseUrl } from "@utils/misc"; import { parseUrl } from "@utils/misc";
import { wordsFromPascal, wordsToTitle } from "@utils/text"; import { wordsFromPascal, wordsToTitle } from "@utils/text";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import previewExampleText from "~fileContent/previewExample.tsx"; import previewExampleText from "~fileContent/previewExample.tsx";
import cssText from "~fileContent/style.css";
import { Settings } from "../../Vencord"; import { Settings } from "../../Vencord";
import { shiki } from "./api/shiki"; import { shiki } from "./api/shiki";
import { themes } from "./api/themes"; import { themes } from "./api/themes";
import { createHighlighter } from "./components/Highlighter"; import { createHighlighter } from "./components/Highlighter";
import { DeviconSetting, HljsSetting, ShikiSettings, StyleSheets } from "./types"; import deviconStyle from "./devicon.css?managed";
import { clearStyles, removeStyle, setStyle } from "./utils/createStyle"; import { DeviconSetting, HljsSetting, ShikiSettings } from "./types";
import { clearStyles } from "./utils/createStyle";
const themeNames = Object.keys(themes); const themeNames = Object.keys(themes);
const devIconCss = "@import url('https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css');";
const getSettings = () => Settings.plugins.ShikiCodeblocks as ShikiSettings; const getSettings = () => Settings.plugins.ShikiCodeblocks as ShikiSettings;
@ -44,15 +46,14 @@ export default definePlugin({
{ {
find: "codeBlock:{react:function", find: "codeBlock:{react:function",
replacement: { replacement: {
match: /codeBlock:\{react:function\((.),(.),(.)\)\{/, match: /codeBlock:\{react:function\((\i),(\i),(\i)\)\{/,
replace: "$&return Vencord.Plugins.plugins.ShikiCodeblocks.renderHighlighter($1,$2,$3);", replace: "$&return $self.renderHighlighter($1,$2,$3);",
}, },
}, },
], ],
start: async () => { start: async () => {
setStyle(cssText, StyleSheets.Main);
if (getSettings().useDevIcon !== DeviconSetting.Disabled) if (getSettings().useDevIcon !== DeviconSetting.Disabled)
setStyle(devIconCss, StyleSheets.DevIcons); enableStyle(deviconStyle);
await shiki.init(getSettings().customTheme || getSettings().theme); await shiki.init(getSettings().customTheme || getSettings().theme);
}, },
@ -135,8 +136,8 @@ export default definePlugin({
}, },
], ],
onChange: (newValue: DeviconSetting) => { onChange: (newValue: DeviconSetting) => {
if (newValue === DeviconSetting.Disabled) removeStyle(StyleSheets.DevIcons); if (newValue === DeviconSetting.Disabled) disableStyle(deviconStyle);
else setStyle(devIconCss, StyleSheets.DevIcons); else enableStyle(deviconStyle);
}, },
}, },
bgOpacity: { bgOpacity: {

View file

@ -1,8 +1,10 @@
.shiki-container {
border: 4px;
background-color: var(--background-secondary);
}
.shiki-root { .shiki-root {
border-radius: 4px; border-radius: 4px;
/* fallback background */
background-color: var(--background-secondary);
} }
.shiki-root code { .shiki-root code {
@ -19,8 +21,7 @@
border: none; border: none;
} }
.shiki-root [class^='devicon-'], .shiki-devicon {
.shiki-root [class*=' devicon-'] {
margin-right: 8px; margin-right: 8px;
user-select: none; user-select: none;
} }

View file

@ -16,13 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { classNameFactory } from "@api/Styles";
import { hljs } from "@webpack/common"; import { hljs } from "@webpack/common";
import { resolveLang } from "../api/languages"; import { resolveLang } from "../api/languages";
import { HighlighterProps } from "../components/Highlighter"; import { HighlighterProps } from "../components/Highlighter";
import { HljsSetting, ShikiSettings } from "../types"; import { HljsSetting, ShikiSettings } from "../types";
export const cl = (className: string) => `shiki-${className}`; export const cl = classNameFactory("shiki-");
export const shouldUseHljs = ({ export const shouldUseHljs = ({
lang, lang,

View file

@ -16,13 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./spotifyStyles.css";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
import { classes, LazyComponent } from "@utils/misc"; import { classes, LazyComponent } from "@utils/misc";
import { filters, find, findByCodeLazy } from "@webpack"; import { filters, find, findByCodeLazy } from "@webpack";
import { ContextMenu, FluxDispatcher, Forms, Menu, React } from "@webpack/common"; import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState } from "@webpack/common";
import { SpotifyStore, Track } from "./SpotifyStore"; import { SpotifyStore, Track } from "./SpotifyStore";
@ -142,10 +144,10 @@ function SeekBar() {
() => [SpotifyStore.mPosition, SpotifyStore.isSettingPosition, SpotifyStore.isPlaying] () => [SpotifyStore.mPosition, SpotifyStore.isSettingPosition, SpotifyStore.isPlaying]
); );
const [position, setPosition] = React.useState(storePosition); const [position, setPosition] = useState(storePosition);
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
React.useEffect(() => { useEffect(() => {
if (isPlaying && !isSettingPosition) { if (isPlaying && !isSettingPosition) {
setPosition(SpotifyStore.position); setPosition(SpotifyStore.position);
const interval = setInterval(() => { const interval = setInterval(() => {
@ -232,7 +234,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
function Info({ track }: { track: Track; }) { function Info({ track }: { track: Track; }) {
const img = track?.album?.image; const img = track?.album?.image;
const [coverExpanded, setCoverExpanded] = React.useState(false); const [coverExpanded, setCoverExpanded] = useState(false);
const i = ( const i = (
<> <>
@ -327,7 +329,7 @@ export function Player() {
); );
const isPlaying = useStateFromStores([SpotifyStore], () => SpotifyStore.isPlaying); const isPlaying = useStateFromStores([SpotifyStore], () => SpotifyStore.isPlaying);
const [shouldHide, setShouldHide] = React.useState(false); const [shouldHide, setShouldHide] = useState(false);
// Hide player after 5 minutes of inactivity // Hide player after 5 minutes of inactivity
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return

View file

@ -21,8 +21,6 @@ import { proxyLazy } from "@utils/proxyLazy";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Flux, FluxDispatcher } from "@webpack/common"; import { Flux, FluxDispatcher } from "@webpack/common";
import cssText from "~fileContent/spotifyStyles.css";
export interface Track { export interface Track {
id: string; id: string;
name: string; name: string;
@ -69,11 +67,6 @@ type Repeat = "off" | "track" | "context";
// Don't wanna run before Flux and Dispatcher are ready! // Don't wanna run before Flux and Dispatcher are ready!
export const SpotifyStore = proxyLazy(() => { export const SpotifyStore = proxyLazy(() => {
// TODO: Move this elsewhere
const style = document.createElement("style");
style.innerText = cssText;
document.head.appendChild(style);
// For some reason ts hates extends Flux.Store // For some reason ts hates extends Flux.Store
const { Store } = Flux; const { Store } = Flux;

View file

@ -28,8 +28,8 @@ export default definePlugin({
patches: [{ patches: [{
find: "PAYMENT_FLOW_MODAL_TEST_PAGE,", find: "PAYMENT_FLOW_MODAL_TEST_PAGE,",
replacement: { replacement: {
match: /({section:[\w.]+?\.PAYMENT_FLOW_MODAL_TEST_PAGE,)/, match: /{section:.{1,2}\..{1,3}\.PAYMENT_FLOW_MODAL_TEST_PAGE/,
replace: '{section:"StartupTimings",label:"Startup Timings",element:Vencord.Plugins.plugins.StartupTimings.StartupTimingPage},$1' replace: '{section:"StartupTimings",label:"Startup Timings",element:Vencord.Plugins.plugins.StartupTimings.StartupTimingPage},$&'
} }
}], }],
StartupTimingPage: LazyComponent(() => require("./StartupTimingPage").default) StartupTimingPage: LazyComponent(() => require("./StartupTimingPage").default)

View file

@ -44,6 +44,34 @@ contextBridge.exposeInMainWorld("VencordNative", VencordNative);
if (location.protocol !== "data:") { if (location.protocol !== "data:") {
// Discord // Discord
webFrame.executeJavaScript(readFileSync(join(__dirname, "renderer.js"), "utf-8")); webFrame.executeJavaScript(readFileSync(join(__dirname, "renderer.js"), "utf-8"));
const rendererCss = join(__dirname, "renderer.css");
function insertCss(css: string) {
const style = document.createElement("style");
style.id = "vencord-css-core";
style.textContent = css;
if (document.readyState === "complete") {
document.documentElement.appendChild(style);
} else {
document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(style), {
once: true
});
}
}
try {
const css = readFileSync(rendererCss, "utf-8");
insertCss(css);
} catch (err) {
if ((err as NodeJS.ErrnoException)?.code !== "ENOENT")
throw err;
// hack: the pre update updater does not download this file, so manually download it
// TODO: remove this in a future version
ipcRenderer.invoke(IpcEvents.DOWNLOAD_VENCORD_CSS)
.then(insertCss);
}
require(process.env.DISCORD_PRELOAD!); require(process.env.DISCORD_PRELOAD!);
} else { } else {
// Monaco Popout // Monaco Popout

View file

@ -44,5 +44,6 @@ export default strEnum({
UPDATE: "VencordUpdate", UPDATE: "VencordUpdate",
BUILD: "VencordBuild", BUILD: "VencordBuild",
GET_DESKTOP_CAPTURE_SOURCES: "VencordGetDesktopCaptureSources", GET_DESKTOP_CAPTURE_SOURCES: "VencordGetDesktopCaptureSources",
OPEN_MONACO_EDITOR: "VencordOpenMonacoEditor" OPEN_MONACO_EDITOR: "VencordOpenMonacoEditor",
DOWNLOAD_VENCORD_CSS: "VencordDownloadVencordCss"
} as const); } as const);

View file

@ -24,7 +24,7 @@ export const REACT_GLOBAL = "Vencord.Webpack.Common.React";
export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`; export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`;
// Add yourself here if you made a plugin // Add yourself here if you made a plugin
export const Devs = Object.freeze({ export const Devs = /* #__PURE__*/ Object.freeze({
Ven: { Ven: {
name: "Vendicated", name: "Vendicated",
id: 343383572805058560n id: 343383572805058560n
@ -177,4 +177,8 @@ export const Devs = Object.freeze({
name: "'ax", name: "'ax",
id: 273562710745284628n, id: 273562710745284628n,
}, },
pointy: {
name: "pointy",
id: 99914384989519872n
}
}); });

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Clipboard, React, Toasts } from "@webpack/common"; import { Clipboard, React, Toasts, useEffect, useState } from "@webpack/common";
/** /**
* Makes a lazy function. On first call, the value is computed. * Makes a lazy function. On first call, the value is computed.
@ -48,13 +48,13 @@ export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterO
deps: [], deps: [],
onError: null, onError: null,
}, providedOpts); }, providedOpts);
const [state, setState] = React.useState({ const [state, setState] = useState({
value: opts.fallbackValue, value: opts.fallbackValue,
error: null, error: null,
pending: true pending: true
}); });
React.useEffect(() => { useEffect(() => {
let isAlive = true; let isAlive = true;
if (!state.pending) setState({ ...state, pending: true }); if (!state.pending) setState({ ...state, pending: true });
@ -72,7 +72,7 @@ export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterO
* Returns a function that can be used to force rerender react components * Returns a function that can be used to force rerender react components
*/ */
export function useForceUpdater() { export function useForceUpdater() {
const [, set] = React.useState(0); const [, set] = useState(0);
return () => set(s => s + 1); return () => set(s => s + 1);
} }

55
src/utils/patches.ts Normal file
View file

@ -0,0 +1,55 @@
/*
* 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 { PatchReplacement } from "./types";
export type ReplaceFn = (match: string, ...groups: string[]) => string;
export function canonicalizeMatch(match: RegExp | string) {
if (typeof match === "string") return match;
const canonSource = match.source
.replaceAll("\\i", "[A-Za-z_$][\\w$]*");
return new RegExp(canonSource, match.flags);
}
export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string) {
if (typeof replace === "function") return replace;
return replace.replaceAll("$self", `Vencord.Plugins.plugins.${pluginName}`);
}
export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>, canonicalize: (value: T) => T) {
if (descriptor.get) {
const original = descriptor.get;
descriptor.get = function () {
return canonicalize(original.call(this));
};
} else if (descriptor.value) {
descriptor.value = canonicalize(descriptor.value);
}
return descriptor;
}
export function canonicalizeReplacement(replacement: Pick<PatchReplacement, "match" | "replace">, plugin: string) {
const descriptors = Object.getOwnPropertyDescriptors(replacement);
descriptors.match = canonicalizeDescriptor(descriptors.match, canonicalizeMatch);
descriptors.replace = canonicalizeDescriptor(
descriptors.replace,
replace => canonicalizeReplace(replace, plugin),
);
Object.defineProperties(replacement, descriptors);
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { React } from "@webpack/common"; import { React, useState } from "@webpack/common";
import { checkIntersecting } from "./misc"; import { checkIntersecting } from "./misc";
@ -30,7 +30,7 @@ export const useIntersection = (intersectOnly = false): [
isIntersecting: boolean, isIntersecting: boolean,
] => { ] => {
const observerRef = React.useRef<IntersectionObserver | null>(null); const observerRef = React.useRef<IntersectionObserver | null>(null);
const [isIntersecting, setIntersecting] = React.useState(false); const [isIntersecting, setIntersecting] = useState(false);
const refCallback = (element: Element | null) => { const refCallback = (element: Element | null) => {
observerRef.current?.disconnect(); observerRef.current?.disconnect();

View file

@ -19,6 +19,8 @@
import { Command } from "@api/Commands"; import { Command } from "@api/Commands";
import { Promisable } from "type-fest"; import { Promisable } from "type-fest";
import type { ReplaceFn } from "./patches";
// exists to export default definePlugin({...}) // exists to export default definePlugin({...})
export default function definePlugin<P extends PluginDef>(p: P & Record<string, any>) { export default function definePlugin<P extends PluginDef>(p: P & Record<string, any>) {
return p; return p;
@ -26,7 +28,7 @@ export default function definePlugin<P extends PluginDef>(p: P & Record<string,
export interface PatchReplacement { export interface PatchReplacement {
match: string | RegExp; match: string | RegExp;
replace: string | ((match: string, ...groups: string[]) => string); replace: string | ReplaceFn;
predicate?(): boolean; predicate?(): boolean;
} }

View file

@ -61,7 +61,7 @@ export function getRepo() {
return Unwrap(VencordNative.ipc.invoke<IpcRes<string>>(IpcEvents.GET_REPO)); return Unwrap(VencordNative.ipc.invoke<IpcRes<string>>(IpcEvents.GET_REPO));
} }
type Hashes = Record<"patcher.js" | "preload.js" | "renderer.js", string>; type Hashes = Record<"patcher.js" | "preload.js" | "renderer.js" | "renderer.css", string>;
/** /**
* @returns true if hard restart is required * @returns true if hard restart is required

View file

@ -31,7 +31,13 @@ export const Margins = findByPropsLazy("marginTop20");
export let FluxDispatcher: Other.FluxDispatcher; export let FluxDispatcher: Other.FluxDispatcher;
export const Flux = findByPropsLazy("connectStores"); export const Flux = findByPropsLazy("connectStores");
export let React: typeof import("react"); export let React: typeof import("react");
export let useState: typeof React.useState;
export let useEffect: typeof React.useEffect;
export let useMemo: typeof React.useMemo;
export let useRef: typeof React.useRef;
export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPortal", "render"); export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPortal", "render");
export const RestAPI = findByPropsLazy("getAPIBaseURL", "get"); export const RestAPI = findByPropsLazy("getAPIBaseURL", "get");
@ -76,7 +82,7 @@ export const TextArea = findByCodeLazy("handleSetRef", "textArea") as React.Comp
export const Select = LazyComponent(() => findByCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems")); export const Select = LazyComponent(() => findByCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
export const Slider = LazyComponent(() => findByCode("closestMarkerIndex", "stickToMarkers")); export const Slider = LazyComponent(() => findByCode("closestMarkerIndex", "stickToMarkers"));
export let SnowflakeUtils: { fromTimestamp: (timestamp: number) => string, extractTimestamp: (snowflake: string) => number }; export let SnowflakeUtils: { fromTimestamp: (timestamp: number) => string, extractTimestamp: (snowflake: string) => number; };
waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m); waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m);
export let Parser: any; export let Parser: any;
@ -151,7 +157,10 @@ export const NavigationRouter = mapMangledModuleLazy("Transitioning to external
goForward: filters.byCode("goForward()"), goForward: filters.byCode("goForward()"),
}); });
waitFor("useState", m => React = m); waitFor("useState", m => {
React = m;
({ useEffect, useState, useMemo, useRef } = React);
});
waitFor(["dispatch", "subscribe"], m => { waitFor(["dispatch", "subscribe"], m => {
FluxDispatcher = m; FluxDispatcher = m;

View file

@ -18,6 +18,8 @@
import { WEBPACK_CHUNK } from "@utils/constants"; import { WEBPACK_CHUNK } from "@utils/constants";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { canonicalizeReplacement } from "@utils/patches";
import { PatchReplacement } from "@utils/types";
import { _initWebpack } from "."; import { _initWebpack } from ".";
@ -135,15 +137,17 @@ function patchPush() {
if (code.includes(patch.find)) { if (code.includes(patch.find)) {
patchedBy.add(patch.plugin); patchedBy.add(patch.plugin);
// @ts-ignore we change all patch.replacement to array in plugins/index // we change all patch.replacement to array in plugins/index
for (const replacement of patch.replacement) { for (const replacement of patch.replacement as PatchReplacement[]) {
if (replacement.predicate && !replacement.predicate()) continue; if (replacement.predicate && !replacement.predicate()) continue;
const lastMod = mod; const lastMod = mod;
const lastCode = code; const lastCode = code;
canonicalizeReplacement(replacement, patch.plugin);
try { try {
const newCode = code.replace(replacement.match, replacement.replace); const newCode = code.replace(replacement.match, replacement.replace as string);
if (newCode === code && !replacement.noWarn) { if (newCode === code && !patch.noWarn) {
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
if (IS_DEV) { if (IS_DEV) {
logger.debug("Function Source:\n", code); logger.debug("Function Source:\n", code);

View file

@ -31,13 +31,15 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
} }
} }
const CANARY = process.env.USE_CANARY === "true";
const browser = await pup.launch({ const browser = await pup.launch({
headless: true, headless: true,
executablePath: process.env.CHROMIUM_BIN executablePath: process.env.CHROMIUM_BIN
}); });
const page = await browser.newPage(); const page = await browser.newPage();
await page.setUserAgent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"); await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
function maybeGetError(handle: JSHandle) { function maybeGetError(handle: JSHandle) {
return (handle as JSHandle<Error>)?.getProperty("message") return (handle as JSHandle<Error>)?.getProperty("message")
@ -65,7 +67,7 @@ function toCodeBlock(s: string) {
} }
async function printReport() { async function printReport() {
console.log("# Vencord Report"); console.log("# Vencord Report" + (CANARY ? " (Canary)" : ""));
console.log(); console.log();
console.log("## Bad Patches"); console.log("## Bad Patches");
@ -98,7 +100,7 @@ async function printReport() {
}, },
body: JSON.stringify({ body: JSON.stringify({
description: "Here's the latest Vencord Report!", description: "Here's the latest Vencord Report!",
username: "Vencord Reporter", username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/f0204a918c6c9c9a43195997e97d8adf.webp", avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/f0204a918c6c9c9a43195997e97d8adf.webp",
embeds: [ embeds: [
{ {
@ -271,4 +273,4 @@ await page.evaluateOnNewDocument(`
;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); ;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
`); `);
await page.goto("https://discord.com/login"); await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");