Merge branch 'dev' into strict-csp

This commit is contained in:
Vending Machine 2025-04-08 00:54:18 +02:00 • committed by GitHub
commit d48f0d0744
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
225 changed files with 4843 additions and 4074 deletions

View file

@ -2,30 +2,24 @@ name: Blank Issue
description: Create a blank issue. ALWAYS FIRST USE OUR SUPPORT CHANNEL! ONLY USE THIS FORM IF YOU ARE A CONTRIBUTOR OR WERE TOLD TO DO SO IN THE SUPPORT CHANNEL.
body:
- type: markdown
attributes:
value: |
# READ THIS BEFORE OPENING AN ISSUE
- type: markdown
attributes:
value: |
![Are you a developer? No? This form is not for you!](https://github.com/Vendicated/Vencord/blob/main/.github/ISSUE_TEMPLATE/developer-banner.png?raw=true)
This form is ONLY FOR DEVELOPERS. YOUR ISSUE WILL BE CLOSED AND YOU WILL POSSIBLY BE BLOCKED FROM THE REPOSITORY IF YOU IGNORE THIS.
DO NOT USE THIS FORM, unless
- you are a vencord contributor
- you were given explicit permission to use this form by a moderator in our support server
DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new)
GitHub Issues are for development, not support! Please use our [support server](https://vencord.dev/discord) unless you are a Vencord Developer.
- type: textarea
id: content
attributes:
label: Content
validations:
required: true
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
options:
- label: I have read the requirements for opening an issue above
- type: textarea
id: content
attributes:
label: Content
validations:
required: true
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
options:
- label: I have read the requirements for opening an issue above
required: true

View file

@ -4,78 +4,63 @@ labels: [bug]
title: "[Bug] <title>"
body:
- type: markdown
attributes:
value: |
# READ THIS BEFORE OPENING AN ISSUE
- type: markdown
attributes:
value: |
![Are you a developer? No? This form is not for you!](https://github.com/Vendicated/Vencord/blob/main/.github/ISSUE_TEMPLATE/developer-banner.png?raw=true)
This form is ONLY FOR DEVELOPERS. YOUR ISSUE WILL BE CLOSED AND YOU WILL POSSIBLY BE BLOCKED FROM THE REPOSITORY IF YOU IGNORE THIS.
DO NOT USE THIS FORM, unless
- you are a vencord contributor
- you were given explicit permission to use this form by a moderator in our support server
DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new)
- type: input
id: discord
attributes:
label: Discord Account
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
placeholder: username#0000
validations:
required: false
GitHub Issues are for development, not support! Please use our [support server](https://vencord.dev/discord) unless you are a Vencord Developer.
- type: textarea
id: bug-description
attributes:
label: What happens when the bug or crash occurs?
description: Where does this bug or crash occur, when does it occur, etc.
placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ...
validations:
required: true
- type: textarea
id: expected-behaviour
attributes:
label: What is the expected behaviour?
description: Simply detail what the expected behaviour is.
placeholder: I expect Vencord/Discord to open the ... page instead of ..., it prevents me from doing ...
validations:
required: true
- type: textarea
id: steps-to-take
attributes:
label: How do you recreate this bug or crash?
description: Give us a list of steps in order to recreate the bug or crash.
placeholder: |
1. Do ...
2. Then ...
3. Do this ..., ... and then ...
4. Observe "the bug" or "the crash"
validations:
required: true
- type: textarea
id: crash-log
attributes:
label: Errors
description: Open the Developer Console with Ctrl/Cmd + Shift + i. Then look for any red errors (Ignore network errors like Failed to load resource) and paste them between the "```".
value: |
```
Replace this text with your crash-log.
```
validations:
required: false
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
description: We only accept reports for bugs that happen on Discord Stable. Canary and PTB are Development branches and may be unstable
options:
- label: I am using Discord Stable or tried on Stable and this bug happens there as well
- type: textarea
id: bug-description
attributes:
label: What happens when the bug or crash occurs?
description: Where does this bug or crash occur, when does it occur, etc.
placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ...
validations:
required: true
- label: I have read the requirements for opening an issue above
- type: textarea
id: expected-behaviour
attributes:
label: What is the expected behaviour?
description: Simply detail what the expected behaviour is.
placeholder: I expect Vencord/Discord to open the ... page instead of ..., it prevents me from doing ...
validations:
required: true
- type: textarea
id: steps-to-take
attributes:
label: How do you recreate this bug or crash?
description: Give us a list of steps in order to recreate the bug or crash.
placeholder: |
1. Do ...
2. Then ...
3. Do this ..., ... and then ...
4. Observe "the bug" or "the crash"
validations:
required: true
- type: textarea
id: crash-log
attributes:
label: Errors
description: Open the Developer Console with Ctrl/Cmd + Shift + i. Then look for any red errors (Ignore network errors like Failed to load resource) and paste them between the "```".
value: |
```
Replace this text with your crash-log.
```
validations:
required: false
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
description: We only accept reports for bugs that happen on Discord Stable. Canary and PTB are Development branches and may be unstable
options:
- label: I am using Discord Stable or tried on Stable and this bug happens there as well
required: true
- label: I am a Vencord Developer
required: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -42,7 +42,7 @@ jobs:
- name: Clean up obsolete files
run: |
rm -rf dist/*-unpacked dist/monaco Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
rm -rf dist/*-unpacked dist/vendor Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
- name: Get some values needed for the release
id: release_values

View file

@ -1,9 +1,22 @@
name: Test Patches
on:
workflow_dispatch:
schedule:
# Every day at midnight
- cron: 0 0 * * *
inputs:
discord_branch:
type: choice
description: "Discord Branch to test patches on"
options:
- both
- stable
- canary
default: both
webhook_url:
type: string
description: "Webhook URL that the report will be posted to. This will be visible for everyone, so DO NOT pass sensitive webhooks like discord webhook. This is meant to be used by Venbot."
required: false
# schedule:
# # Every day at midnight
# - cron: 0 0 * * *
jobs:
TestPlugins:
@ -40,28 +53,43 @@ jobs:
- name: Build Vencord Reporter Version
run: pnpm buildReporter
- name: Create Report
- name: Run Reporter
timeout-minutes: 10
run: |
export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
esbuild scripts/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
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=${{ steps.setup-chrome.outputs.chrome-path }}
export USE_CANARY=true
stable_output_file=$(mktemp)
canary_output_file=$(mktemp)
esbuild scripts/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
pids=""
branch="${{ inputs.discord_branch }}"
if [[ "${{ github.event_name }}" = "schedule" ]]; then
branch="both"
fi
if [[ "$branch" = "both" || "$branch" = "stable" ]]; then
node dist/report.mjs > "$stable_output_file" &
pids+=" $!"
fi
if [[ "$branch" = "both" || "$branch" = "canary" ]]; then
USE_CANARY=true node dist/report.mjs > "$canary_output_file" &
pids+=" $!"
fi
exit_code=0
for pid in $pids; do
if ! wait "$pid"; then
exit_code=1
fi
done
cat "$stable_output_file" "$canary_output_file" >> $GITHUB_STEP_SUMMARY
exit $exit_code
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
WEBHOOK_URL: ${{ inputs.webhook_url || secrets.DISCORD_WEBHOOK }}
WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }}

1
.gitignore vendored
View file

@ -8,6 +8,7 @@ vencord_installer
.DS_Store
yarn.lock
bun.lock
package-lock.json
*.log

View file

@ -36,7 +36,7 @@
"web_accessible_resources": [
{
"resources": ["dist/*", "third-party/*"],
"resources": ["dist/*", "vendor/*"],
"matches": ["*://*.discord.com/*"]
}
],

View file

@ -15,7 +15,7 @@ declare global {
const getTheme: () => string;
}
const BASE = "/dist/monaco/vs";
const BASE = "/vendor/monaco/vs";
self.MonacoEnvironment = {
getWorkerUrl(_moduleId: unknown, label: string) {

View file

@ -24,12 +24,12 @@
<script>
const script = document.createElement("script");
script.src = new URL("/dist/monaco/index.js", baseUrl);
script.src = new URL("/vendor/monaco/index.js", baseUrl);
const style = document.createElement("link");
style.type = "text/css";
style.rel = "stylesheet";
style.href = new URL("/dist/monaco/index.css", baseUrl);
style.href = new URL("/vendor/monaco/index.css", baseUrl);
document.body.append(style, script);
</script>

View file

@ -134,7 +134,7 @@ export default tseslint.config(
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-const": ["error", { destructuring: "all" }],
"prefer-spread": "error",
// Plugin Rules

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.11.2",
"version": "1.11.8",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -24,66 +24,65 @@
"dev": "pnpm watch",
"watchWeb": "pnpm buildWeb --watch",
"generatePluginJson": "tsx scripts/generatePluginList.ts",
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
"inject": "node scripts/runInstaller.mjs",
"uninject": "node scripts/runInstaller.mjs",
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types --allowJs false",
"inject": "node scripts/runInstaller.mjs -- --install",
"uninject": "node scripts/runInstaller.mjs -- --uninstall",
"lint": "eslint",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix",
"test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
"test": "pnpm buildStandalone && pnpm testTsc && pnpm lint && pnpm lint-styles && pnpm generatePluginJson",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit"
},
"dependencies": {
"@intrnl/xxhash64": "^0.1.2",
"@sapphi-red/web-noise-suppressor": "0.3.5",
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.5",
"fflate": "^0.8.2",
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"monaco-editor": "^0.52.2",
"nanoid": "^5.0.9",
"nanoid": "^5.1.5",
"virtual-merge": "^1.0.1"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^2.12.1",
"@types/chrome": "^0.0.287",
"@types/diff": "^6.0.0",
"@stylistic/eslint-plugin": "^4.2.0",
"@types/chrome": "^0.0.312",
"@types/diff": "^7.0.2",
"@types/lodash": "^4.17.14",
"@types/node": "^22.10.5",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@types/node": "^22.13.13",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/yazl": "^2.4.5",
"diff": "^7.0.0",
"discord-types": "^1.3.26",
"esbuild": "^0.15.18",
"eslint": "^9.17.0",
"esbuild": "^0.25.1",
"eslint": "9.20.1",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "2.1.0",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-simple-header": "^1.2.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4",
"highlight.js": "11.7.0",
"highlight.js": "11.11.1",
"html-minifier-terser": "^7.2.0",
"moment": "^2.22.2",
"puppeteer-core": "^23.11.1",
"standalone-electron-types": "^1.0.0",
"stylelint": "^16.12.0",
"stylelint-config-standard": "^36.0.1",
"puppeteer-core": "^24.4.0",
"standalone-electron-types": "^34.2.0",
"stylelint": "^16.17.0",
"stylelint-config-standard": "^37.0.0",
"ts-patch": "^3.3.0",
"ts-pattern": "^5.6.0",
"tsx": "^4.19.2",
"type-fest": "^4.31.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.19.0",
"typescript-transform-paths": "^3.5.3",
"tsx": "^4.19.3",
"type-fest": "^4.38.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.28.0",
"typescript-transform-paths": "^3.5.5",
"zip-local": "^0.3.5"
},
"packageManager": "pnpm@9.1.0",
"packageManager": "pnpm@10.4.1",
"pnpm": {
"patchedDependencies": {
"eslint@9.17.0": "patches/eslint@9.17.0.patch",
"eslint@9.20.1": "patches/eslint@9.20.1.patch",
"eslint-plugin-path-alias@2.1.0": "patches/eslint-plugin-path-alias@2.1.0.patch"
},
"peerDependencyRules": {
@ -96,18 +95,14 @@
"source-map-resolve": "*",
"resolve-url": "*",
"source-map-url": "*",
"urix": "*"
}
},
"webExt": {
"artifactsDir": "./dist",
"build": {
"overwriteDest": true
"urix": "*",
"q": "*"
},
"sourceDir": "./dist/firefox-unpacked"
"onlyBuiltDependencies": [
"esbuild"
]
},
"engines": {
"node": ">=18",
"pnpm": ">=9"
"node": ">=18"
}
}

View file

@ -1,7 +1,7 @@
{
"name": "@vencord/types",
"private": false,
"version": "0.1.3",
"version": "1.11.5",
"description": "",
"types": "index.d.ts",
"scripts": {
@ -13,16 +13,16 @@
"license": "GPL-3.0",
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"fs-extra": "^11.2.0",
"tsx": "^3.12.6"
"fs-extra": "^11.3.0",
"tsx": "^4.19.2"
},
"dependencies": {
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.18",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.0.10",
"@types/lodash": "4.17.15",
"@types/node": "^22.13.4",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.1",
"discord-types": "^1.3.26",
"standalone-electron-types": "^1.0.0",
"type-fest": "^3.5.3"
"standalone-electron-types": "^34.2.0",
"type-fest": "^4.35.0"
}
}

2404
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -17,38 +17,41 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import esbuild from "esbuild";
// @ts-check
import { readdir } from "fs/promises";
import { join } from "path";
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch } from "./common.mjs";
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch, buildOrWatchAll, stringifyValues } from "./common.mjs";
const defines = {
const defines = stringifyValues({
IS_STANDALONE,
IS_DEV,
IS_REPORTER,
IS_UPDATER_DISABLED,
IS_WEB: false,
IS_EXTENSION: false,
VERSION: JSON.stringify(VERSION),
VERSION,
BUILD_TIMESTAMP
};
});
if (defines.IS_STANDALONE === false)
if (defines.IS_STANDALONE === "false") {
// If this is a local build (not standalone), optimize
// for the specific platform we're on
defines["process.platform"] = JSON.stringify(process.platform);
}
/**
* @type {esbuild.BuildOptions}
* @type {import("esbuild").BuildOptions}
*/
const nodeCommonOpts = {
...commonOpts,
define: defines,
format: "cjs",
platform: "node",
target: ["esnext"],
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
define: defines
// @ts-ignore this is never undefined
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external]
};
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
@ -102,25 +105,27 @@ const globNativesPlugin = {
}
};
await Promise.all([
/** @type {import("esbuild").BuildOptions[]} */
const buildConfigs = ([
// Discord Desktop main & renderer & preload
esbuild.build({
{
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: true,
IS_VESKTOP: false
},
plugins: [
// @ts-ignore this is never undefined
...nodeCommonOpts.plugins,
globNativesPlugin
]
}),
esbuild.build({
],
define: {
...defines,
IS_DISCORD_DESKTOP: "true",
IS_VESKTOP: "false"
}
},
{
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/renderer.js",
@ -135,11 +140,11 @@ await Promise.all([
],
define: {
...defines,
IS_DISCORD_DESKTOP: true,
IS_VESKTOP: false
IS_DISCORD_DESKTOP: "true",
IS_VESKTOP: "false"
}
}),
esbuild.build({
},
{
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
outfile: "dist/preload.js",
@ -147,29 +152,29 @@ await Promise.all([
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: true,
IS_VESKTOP: false
IS_DISCORD_DESKTOP: "true",
IS_VESKTOP: "false"
}
}),
},
// Vencord Desktop main & renderer & preload
esbuild.build({
{
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/vencordDesktopMain.js",
footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: true
},
plugins: [
...nodeCommonOpts.plugins,
globNativesPlugin
]
}),
esbuild.build({
],
define: {
...defines,
IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: "true"
}
},
{
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/vencordDesktopRenderer.js",
@ -179,16 +184,16 @@ await Promise.all([
globalName: "Vencord",
sourcemap,
plugins: [
globPlugins("vencordDesktop"),
globPlugins("vesktop"),
...commonRendererPlugins
],
define: {
...defines,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: true
IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: "true"
}
}),
esbuild.build({
},
{
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
outfile: "dist/vencordDesktopPreload.js",
@ -196,14 +201,10 @@ await Promise.all([
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: true
IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: "true"
}
}),
]).catch(err => {
console.error("Build failed");
console.error(err.message);
// make ci fail
if (!commonOpts.watch)
process.exitCode = 1;
});
}
]);
await buildOrWatchAll(buildConfigs);

View file

@ -17,29 +17,30 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import esbuild from "esbuild";
// @ts-check
import { readFileSync } from "fs";
import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
import { join } from "path";
import Zip from "zip-local";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION, commonRendererPlugins } from "./common.mjs";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues } from "./common.mjs";
/**
* @type {esbuild.BuildOptions}
* @type {import("esbuild").BuildOptions}
*/
const commonOptions = {
...commonOpts,
entryPoints: ["browser/Vencord.ts"],
globalName: "Vencord",
format: "iife",
globalName: "Vencord",
external: ["~plugins", "~git-hash", "/assets/*"],
target: ["esnext"],
plugins: [
globPlugins("web"),
...commonRendererPlugins
],
target: ["esnext"],
define: {
define: stringifyValues({
IS_WEB: true,
IS_EXTENSION: false,
IS_STANDALONE: true,
@ -48,9 +49,9 @@ const commonOptions = {
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: false,
IS_UPDATER_DISABLED: true,
VERSION: JSON.stringify(VERSION),
VERSION,
BUILD_TIMESTAMP
}
})
};
const MonacoWorkerEntryPoints = [
@ -58,70 +59,59 @@ const MonacoWorkerEntryPoints = [
"vs/editor/editor.worker.js"
];
const RnNoiseFiles = [
"dist/rnnoise.wasm",
"dist/rnnoise_simd.wasm",
"dist/rnnoise/workletProcessor.js",
"LICENSE"
/** @type {import("esbuild").BuildOptions[]} */
const buildConfigs = [
{
entryPoints: MonacoWorkerEntryPoints.map(entry => `node_modules/monaco-editor/esm/${entry}`),
bundle: true,
minify: true,
format: "iife",
outbase: "node_modules/monaco-editor/esm/",
outdir: "dist/vendor/monaco"
},
{
entryPoints: ["browser/monaco.ts"],
bundle: true,
minify: true,
format: "iife",
outfile: "dist/vendor/monaco/index.js",
loader: {
".ttf": "file"
}
},
{
...commonOptions,
outfile: "dist/browser.js",
footer: { js: "//# sourceURL=VencordWeb" }
},
{
...commonOptions,
outfile: "dist/extension.js",
define: {
...commonOptions.define,
IS_EXTENSION: "true"
},
footer: { js: "//# sourceURL=VencordWeb" }
},
{
...commonOptions,
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
define: {
...commonOptions.define,
window: "unsafeWindow",
},
outfile: "dist/Vencord.user.js",
banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${VERSION}.${new Date().getTime()}`)
},
footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
}
}
];
await Promise.all(
[
esbuild.build({
entryPoints: MonacoWorkerEntryPoints.map(entry => `node_modules/monaco-editor/esm/${entry}`),
bundle: true,
minify: true,
format: "iife",
outbase: "node_modules/monaco-editor/esm/",
outdir: "dist/monaco"
}),
esbuild.build({
entryPoints: ["browser/monaco.ts"],
bundle: true,
minify: true,
format: "iife",
outfile: "dist/monaco/index.js",
loader: {
".ttf": "file"
}
}),
esbuild.build({
...commonOptions,
outfile: "dist/browser.js",
footer: { js: "//# sourceURL=VencordWeb" }
}),
esbuild.build({
...commonOptions,
outfile: "dist/extension.js",
define: {
...commonOptions?.define,
IS_EXTENSION: true,
},
footer: { js: "//# sourceURL=VencordWeb" }
}),
esbuild.build({
...commonOptions,
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
define: {
...(commonOptions?.define),
window: "unsafeWindow",
},
outfile: "dist/Vencord.user.js",
banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${VERSION}.${new Date().getTime()}`)
},
footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
}
})
]
).catch(err => {
console.error("Build failed");
console.error(err.message);
if (!commonOpts.watch)
process.exit(1);
});;
await buildOrWatchAll(buildConfigs);
/**
* @type {(dir: string) => Promise<string[]>}
@ -155,16 +145,13 @@ async function buildExtension(target, files) {
const entries = {
"dist/Vencord.js": await readFile("dist/extension.js"),
"dist/Vencord.css": await readFile("dist/extension.css"),
...await loadDir("dist/monaco"),
...Object.fromEntries(await Promise.all(RnNoiseFiles.map(async file =>
[`third-party/rnnoise/${file.replace(/^dist\//, "")}`, await readFile(`node_modules/@sapphi-red/web-noise-suppressor/${file}`)]
))),
...await loadDir("dist/vendor/monaco", "dist/"),
...Object.fromEntries(await Promise.all(files.map(async f => {
let content = await readFile(join("browser", f));
if (f.startsWith("manifest")) {
const json = JSON.parse(content.toString("utf-8"));
json.version = VERSION;
content = new TextEncoder().encode(JSON.stringify(json));
content = Buffer.from(new TextEncoder().encode(JSON.stringify(json)));
}
return [
@ -210,7 +197,6 @@ if (!process.argv.includes("--skip-extension")) {
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
} else {
await appendCssRuntime;
}

View file

@ -16,11 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// @ts-check
import "../suppressExperimentalWarnings.js";
import "../checkNodeVersion.js";
import { exec, execSync } from "child_process";
import esbuild from "esbuild";
import esbuild, { build, context } from "esbuild";
import { constants as FsConstants, readFileSync } from "fs";
import { access, readdir, readFile } from "fs/promises";
import { minify as minifyHtml } from "html-minifier-terser";
@ -31,7 +33,7 @@ import { getPluginTarget } from "../utils.mjs";
import { builtinModules } from "module";
/** @type {import("../../package.json")} */
const PackageJSON = JSON.parse(readFileSync("package.json"));
const PackageJSON = JSON.parse(readFileSync("package.json", "utf-8"));
export const VERSION = PackageJSON.version;
// https://reproducible-builds.org/docs/source-date-epoch/
@ -54,6 +56,34 @@ export const banner = {
`.trim()
};
/**
* JSON.stringify all values in an object
* @type {(obj: Record<string, any>) => Record<string, string>}
*/
export function stringifyValues(obj) {
for (const key in obj) {
obj[key] = JSON.stringify(obj[key]);
}
return obj;
}
/**
* @param {import("esbuild").BuildOptions[]} buildConfigs
*/
export async function buildOrWatchAll(buildConfigs) {
if (watch) {
await Promise.all(buildConfigs.map(cfg =>
context(cfg).then(ctx => ctx.watch())
));
} else {
await Promise.all(buildConfigs.map(cfg => build(cfg)))
.catch(error => {
console.error(error.message);
process.exit(1); // exit immediately to skip the rest of the builds
});
}
}
const PluginDefinitionNameMatcher = /definePlugin\(\{\s*(["'])?name\1:\s*(["'`])(.+?)\2/;
/**
* @param {string} base
@ -99,7 +129,7 @@ export const makeAllPackagesExternalPlugin = {
};
/**
* @type {(kind: "web" | "discordDesktop" | "vencordDesktop") => import("esbuild").Plugin}
* @type {(kind: "web" | "discordDesktop" | "vesktop") => import("esbuild").Plugin}
*/
export const globPlugins = kind => ({
name: "glob-plugins",
@ -138,7 +168,7 @@ export const globPlugins = kind => ({
(target === "web" && kind === "discordDesktop") ||
(target === "desktop" && kind === "web") ||
(target === "discordDesktop" && kind !== "discordDesktop") ||
(target === "vencordDesktop" && kind !== "vencordDesktop");
(target === "vesktop" && kind !== "vesktop");
if (excluded) {
const name = await resolvePluginName(fullDir, file);
@ -311,18 +341,16 @@ export const banImportPlugin = (filter, message) => ({
export const commonOpts = {
logLevel: "info",
bundle: true,
watch,
minify: !watch,
sourcemap: watch ? "inline" : "",
minify: !watch && !IS_REPORTER,
sourcemap: watch ? "inline" : "external",
legalComments: "linked",
banner,
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"],
jsx: "transform",
jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment",
// Work around https://github.com/evanw/esbuild/issues/2460
tsconfig: "./scripts/build/tsconfig.esbuild.json"
jsxFragment: "VencordFragment"
};
const escapedBuiltinModules = builtinModules
@ -335,5 +363,6 @@ export const commonRendererPlugins = [
banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"),
banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"),
banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"),
// @ts-ignore this is never undefined
...commonOpts.plugins
];

View file

@ -1,7 +0,0 @@
// Work around https://github.com/evanw/esbuild/issues/2460
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}

View file

@ -39,7 +39,7 @@ interface PluginData {
hasCommands: boolean;
required: boolean;
enabledByDefault: boolean;
target: "discordDesktop" | "vencordDesktop" | "desktop" | "web" | "dev";
target: "discordDesktop" | "vesktop" | "desktop" | "web" | "dev";
filePath: string;
}
@ -163,7 +163,7 @@ async function parseFile(fileName: string) {
const target = getPluginTarget(fileName);
if (target) {
if (!["web", "discordDesktop", "vencordDesktop", "desktop", "dev"].includes(target)) throw fail(`invalid target ${target}`);
if (!["web", "discordDesktop", "vesktop", "desktop", "dev"].includes(target)) throw fail(`invalid target ${target}`);
data.target = target as any;
}

View file

@ -16,24 +16,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* eslint-disable no-fallthrough */
// eslint-disable-next-line spaced-comment
/// <reference types="../src/globals" />
// eslint-disable-next-line spaced-comment
/// <reference types="../src/modules" />
import { createHmac } from "crypto";
import { readFileSync } from "fs";
import pup, { JSHandle } from "puppeteer-core";
for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
const logStderr = (...data: any[]) => console.error(`${CANARY ? "CANARY" : "STABLE"} ---`, ...data);
for (const variable of ["CHROMIUM_BIN"]) {
if (!process.env[variable]) {
console.error(`Missing environment variable ${variable}`);
logStderr(`Missing environment variable ${variable}`);
process.exit(1);
}
}
const CANARY = process.env.USE_CANARY === "true";
let metaData = {
buildNumber: "Unknown Build Number",
buildHash: "Unknown Build Hash"
};
const browser = await pup.launch({
headless: true,
@ -51,14 +54,17 @@ async function maybeGetError(handle: JSHandle): Promise<string | undefined> {
.catch(() => undefined);
}
interface PatchInfo {
plugin: string;
type: string;
id: string;
match: string;
error?: string;
};
const report = {
badPatches: [] as {
plugin: string;
type: string;
id: string;
match: string;
error?: string;
}[],
badPatches: [] as PatchInfo[],
slowPatches: [] as PatchInfo[],
badStarts: [] as {
plugin: string;
error: string;
@ -128,56 +134,88 @@ async function printReport() {
console.log();
if (process.env.DISCORD_WEBHOOK) {
await fetch(process.env.DISCORD_WEBHOOK, {
method: "POST",
headers: {
"Content-Type": "application/json"
if (process.env.WEBHOOK_URL) {
const patchesToEmbed = (title: string, patches: PatchInfo[], color: number) => ({
title,
color,
description: patches.map(p => {
const lines = [
`**__${p.plugin} (${p.type}):__**`,
`ID: \`${p.id}\``,
`Match: ${toCodeBlock(p.match, "Match: ".length, true)}`
];
if (p.error) lines.push(`Error: ${toCodeBlock(p.error, "Error: ".length, true)}`);
return lines.join("\n");
}).join("\n\n"),
});
const embeds = [
{
author: {
name: `Discord ${CANARY ? "Canary" : "Stable"} (${metaData.buildNumber})`,
url: `https://nelly.tools/builds/app/${metaData.buildHash}`,
icon_url: CANARY ? "https://cdn.discordapp.com/emojis/1252721945699549327.png?size=128" : "https://cdn.discordapp.com/emojis/1252721943463985272.png?size=128"
},
color: CANARY ? 0xfbb642 : 0x5865f2
},
body: JSON.stringify({
description: "Here's the latest Vencord Report!",
username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
embeds: [
{
title: "Bad Patches",
description: report.badPatches.map(p => {
const lines = [
`**__${p.plugin} (${p.type}):__**`,
`ID: \`${p.id}\``,
`Match: ${toCodeBlock(p.match, "Match: ".length, true)}`
];
if (p.error) lines.push(`Error: ${toCodeBlock(p.error, "Error: ".length, true)}`);
return lines.join("\n");
}).join("\n\n") || "None",
color: report.badPatches.length ? 0xff0000 : 0x00ff00
},
{
title: "Bad Webpack Finds",
description: report.badWebpackFinds.map(f => toCodeBlock(f, 0, true)).join("\n") || "None",
color: report.badWebpackFinds.length ? 0xff0000 : 0x00ff00
},
{
title: "Bad Starts",
description: report.badStarts.map(p => {
const lines = [
`**__${p.plugin}:__**`,
toCodeBlock(p.error, 0, true)
];
return lines.join("\n");
}
).join("\n\n") || "None",
color: report.badStarts.length ? 0xff0000 : 0x00ff00
},
{
title: "Discord Errors",
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n"), 0, true) : "None",
color: report.otherErrors.length ? 0xff0000 : 0x00ff00
}
]
})
report.badPatches.length > 0 && patchesToEmbed("Bad Patches", report.badPatches, 0xff0000),
report.slowPatches.length > 0 && patchesToEmbed("Slow Patches", report.slowPatches, 0xf0b232),
report.badWebpackFinds.length > 0 && {
title: "Bad Webpack Finds",
description: report.badWebpackFinds.map(f => toCodeBlock(f, 0, true)).join("\n") || "None",
color: 0xff0000
},
report.badStarts.length > 0 && {
title: "Bad Starts",
description: report.badStarts.map(p => {
const lines = [
`**__${p.plugin}:__**`,
toCodeBlock(p.error, 0, true)
];
return lines.join("\n");
}
).join("\n\n") || "None",
color: 0xff0000
},
report.otherErrors.length > 0 && {
title: "Discord Errors",
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n"), 0, true) : "None",
color: 0xff0000
}
].filter(Boolean);
if (embeds.length === 1) {
embeds.push({
title: "No issues found",
description: "Seems like everything is working fine (for now) <:shipit:1330992641466433556>",
color: 0x00ff00
});
}
const body = JSON.stringify({
username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
embeds
});
const headers = {
"Content-Type": "application/json"
};
// functions similar to https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
// used by venbot to ensure webhook invocations are genuine (since we will pass the webhook url as a workflow input which is publicly visible)
// generate a secret with something like `openssl rand -hex 128`
if (process.env.WEBHOOK_SECRET) {
headers["X-Signature"] = "sha256=" + createHmac("sha256", process.env.WEBHOOK_SECRET).update(body).digest("hex");
}
await fetch(process.env.WEBHOOK_URL, {
method: "POST",
headers,
body
}).then(res => {
if (!res.ok) console.error(`Webhook failed with status ${res.status}`);
else console.error("Posted to Discord Webhook successfully");
if (!res.ok) logStderr(`Webhook failed with status ${res.status}`);
else logStderr("Posted to Webhook successfully");
});
}
}
@ -186,10 +224,13 @@ page.on("console", async e => {
const level = e.type();
const rawArgs = e.args();
async function getText() {
async function getText(skipFirst = true) {
let args = e.args();
if (skipFirst) args = args.slice(1);
try {
return await Promise.all(
e.args().map(async a => {
args.map(async a => {
return await maybeGetError(a) || await a.jsonValue();
})
).then(a => a.join(" ").trim());
@ -202,6 +243,12 @@ page.on("console", async e => {
const isVencord = firstArg === "[Vencord]";
const isDebug = firstArg === "[PUP_DEBUG]";
const isReporterMeta = firstArg === "[REPORTER_META]";
if (isReporterMeta) {
metaData = await rawArgs[1].jsonValue() as any;
return;
}
outer:
if (isVencord) {
@ -215,18 +262,21 @@ page.on("console", async e => {
switch (tag) {
case "WebpackInterceptor:":
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
if (!patchFailMatch) break;
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/);
const patchSlowMatch = message.match(/Patch by (.+?) (took [\d.]+?ms) \(Module id is (.+?)\): (.+)/);
const match = patchFailMatch ?? patchSlowMatch;
if (!match) break;
console.error(await getText());
logStderr(await getText());
process.exitCode = 1;
const [, plugin, type, id, regex] = patchFailMatch;
report.badPatches.push({
const [, plugin, type, id, regex] = match;
const list = patchFailMatch ? report.badPatches : report.slowPatches;
list.push({
plugin,
type,
id,
match: regex.replace(/\(\?:\[A-Za-z_\$\]\[\\w\$\]\*\)/g, "\\i"),
match: regex,
error: await maybeGetError(e.args()[3])
});
@ -235,7 +285,7 @@ page.on("console", async e => {
const failedToStartMatch = message.match(/Failed to start (.+)/);
if (!failedToStartMatch) break;
console.error(await getText());
logStderr(await getText());
process.exitCode = 1;
const [, name] = failedToStartMatch;
@ -246,7 +296,7 @@ page.on("console", async e => {
break;
case "LazyChunkLoader:":
console.error(await getText());
logStderr(await getText());
switch (message) {
case "A fatal error occurred:":
@ -255,7 +305,7 @@ page.on("console", async e => {
break;
case "Reporter:":
console.error(await getText());
logStderr(await getText());
switch (message) {
case "A fatal error occurred:":
@ -273,47 +323,36 @@ page.on("console", async e => {
}
if (isDebug) {
console.error(await getText());
logStderr(await getText());
} else if (level === "error") {
const text = await getText();
const text = await getText(false);
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) {
report.ignoredErrors.push(text);
} else {
console.error("[Unexpected Error]", text);
logStderr("[Unexpected Error]", text);
report.otherErrors.push(text);
}
}
}
});
page.on("error", e => console.error("[Error]", e.message));
page.on("error", e => logStderr("[Error]", e.message));
page.on("pageerror", e => {
if (e.message.includes("Sentry successfully disabled")) return;
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
console.error("[Page Error]", e.message);
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module") && !/^.{1,2}$/.test(e.message)) {
logStderr("[Page Error]", e.message);
report.otherErrors.push(e.message);
} else {
report.ignoredErrors.push(e.message);
}
});
async function reporterRuntime(token: string) {
Vencord.Webpack.waitFor(
"loginToken",
m => {
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}
);
}
await page.evaluateOnNewDocument(`
if (location.host.endsWith("discord.com")) {
${readFileSync("./dist/browser.js", "utf-8")};
(${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
}
`);

View file

@ -118,8 +118,11 @@ const installerBin = await ensureBinary();
console.log("Now running Installer...");
const argStart = process.argv.indexOf("--");
const args = argStart === -1 ? [] : process.argv.slice(argStart + 1);
try {
execFileSync(installerBin, {
execFileSync(installerBin, args, {
stdio: "inherit",
env: {
...process.env,

View file

@ -23,6 +23,7 @@ export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss";
export * as Updater from "./utils/updater";
export * as Webpack from "./webpack";
export * as WebpackPatcher from "./webpack/patchWebpack";
export { PlainSettings, Settings };
import "./utils/quickCss";

View file

@ -4,11 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { Settings } from "@api/Settings";
import { PluginIpcMappings } from "@main/ipcPlugins";
import type { UserThemeHeader } from "@main/themes";
import { IpcEvents } from "@shared/IpcEvents";
import { IpcRes } from "@utils/types";
import type { Settings } from "api/Settings";
import { ipcRenderer } from "electron";
function invoke<T = any>(event: IpcEvents, ...args: any[]) {

View file

@ -9,7 +9,7 @@ import "./ChatButton.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { waitFor } from "@webpack";
import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common";
import { Button, ButtonWrapperClasses, Tooltip } from "@webpack/common";
import { Channel } from "discord-types/general";
import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
@ -110,7 +110,7 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
<Button
aria-label={props.tooltip}
size=""
look={ButtonLooks.BLANK}
look={Button.Looks.BLANK}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`}

View file

@ -31,6 +31,7 @@ export const commands = {} as Record<string, Command>;
// hack for plugins being evaluated before we can grab these from webpack
const OptPlaceholder = Symbol("OptionalMessageOption") as any as Option;
const ReqPlaceholder = Symbol("RequiredMessageOption") as any as Option;
/**
* Optional message option named "message" you can use in commands.
* Used in "tableflip" or "shrug"
@ -44,11 +45,16 @@ export let OptionalMessageOption: Option = OptPlaceholder;
*/
export let RequiredMessageOption: Option = ReqPlaceholder;
// Discord's command list has random gaps for some reason, which can cause issues while rendering the commands
// Add this offset to every added command to keep them unique
let commandIdOffset: number;
export const _init = function (cmds: Command[]) {
try {
BUILT_IN = cmds;
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
commandIdOffset = Math.abs(BUILT_IN.map(x => Number(x.id)).sort((x, y) => x - y)[0]) - BUILT_IN.length;
} catch (e) {
new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
}
@ -142,7 +148,7 @@ export function registerCommand<C extends Command>(command: C, plugin: string) {
command.isVencordCommand = true;
command.untranslatedName ??= command.name;
command.untranslatedDescription ??= command.description;
command.id ??= `-${BUILT_IN.length + 1}`;
command.id ??= `-${BUILT_IN.length + commandIdOffset + 1}`;
command.applicationId ??= "-1"; // BUILT_IN;
command.type ??= ApplicationCommandType.CHAT_INPUT;
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;

View file

@ -122,7 +122,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
}
interface ContextMenuProps {
contextMenuApiArguments?: Array<any>;
contextMenuAPIArguments?: Array<any>;
navId: string;
children: Array<ReactElement<any> | null>;
"aria-label": string;
@ -136,7 +136,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
children: cloneMenuChildren(props.children),
};
props.contextMenuApiArguments ??= [];
props.contextMenuAPIArguments ??= [];
const contextMenuPatches = navPatches.get(props.navId);
if (!Array.isArray(props.children)) props.children = [props.children];
@ -144,7 +144,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
if (contextMenuPatches) {
for (const patch of contextMenuPatches) {
try {
patch(props.children, ...props.contextMenuApiArguments);
patch(props.children, ...props.contextMenuAPIArguments);
} catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
}
@ -153,7 +153,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
for (const patch of globalPatches) {
try {
patch(props.navId, props.children, ...props.contextMenuApiArguments);
patch(props.navId, props.children, ...props.contextMenuAPIArguments);
} catch (err) {
ContextMenuLogger.error("Global patch errored,", err);
}

View file

@ -43,20 +43,21 @@ interface DecoratorProps {
export type MemberListDecoratorFactory = (props: DecoratorProps) => JSX.Element | null;
type OnlyIn = "guilds" | "dms";
export const decorators = new Map<string, { render: MemberListDecoratorFactory, onlyIn?: OnlyIn; }>();
export const decoratorsFactories = new Map<string, { render: MemberListDecoratorFactory, onlyIn?: OnlyIn; }>();
export function addMemberListDecorator(identifier: string, render: MemberListDecoratorFactory, onlyIn?: OnlyIn) {
decorators.set(identifier, { render, onlyIn });
decoratorsFactories.set(identifier, { render, onlyIn });
}
export function removeMemberListDecorator(identifier: string) {
decorators.delete(identifier);
decoratorsFactories.delete(identifier);
}
export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] {
export function __getDecorators(props: DecoratorProps): JSX.Element {
const isInGuild = !!(props.guildId);
return Array.from(
decorators.entries(),
const decorators = Array.from(
decoratorsFactories.entries(),
([key, { render: Decorator, onlyIn }]) => {
if ((onlyIn === "guilds" && !isInGuild) || (onlyIn === "dms" && isInGuild))
return null;
@ -68,4 +69,10 @@ export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] {
);
}
);
return (
<div className="vc-member-list-decorators-wrapper">
{decorators}
</div>
);
}

View file

@ -48,23 +48,29 @@ export interface MessageDecorationProps {
}
export type MessageDecorationFactory = (props: MessageDecorationProps) => JSX.Element | null;
export const decorations = new Map<string, MessageDecorationFactory>();
export const decorationsFactories = new Map<string, MessageDecorationFactory>();
export function addMessageDecoration(identifier: string, decoration: MessageDecorationFactory) {
decorations.set(identifier, decoration);
decorationsFactories.set(identifier, decoration);
}
export function removeMessageDecoration(identifier: string) {
decorations.delete(identifier);
decorationsFactories.delete(identifier);
}
export function __addDecorationsToMessage(props: MessageDecorationProps): (JSX.Element | null)[] {
return Array.from(
decorations.entries(),
export function __addDecorationsToMessage(props: MessageDecorationProps): JSX.Element {
const decorations = Array.from(
decorationsFactories.entries(),
([key, Decoration]) => (
<ErrorBoundary noop message={`Failed to render ${key} Message Decoration`} key={key}>
<Decoration {...props} />
</ErrorBoundary>
)
);
return (
<div className="vc-message-decorations-wrapper">
{decorations}
</div>
);
}

View file

@ -11,6 +11,10 @@
width: 100%;
}
.visual-refresh .vc-notification-root {
background-color: var(--bg-overlay-floating, var(--background-base-low));
}
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
position: absolute;
z-index: 2147483647;

View file

@ -32,9 +32,10 @@ export interface Settings {
autoUpdate: boolean;
autoUpdateNotification: boolean,
useQuickCss: boolean;
eagerPatches: boolean;
enabledThemes: string[];
enableReactDevtools: boolean;
themeLinks: string[];
enabledThemes: string[];
frameless: boolean;
transparent: boolean;
winCtrlQ: boolean;
@ -81,6 +82,7 @@ const DefaultSettings: Settings = {
autoUpdateNotification: true,
useQuickCss: true,
themeLinks: [],
eagerPatches: IS_REPORTER,
enabledThemes: [],
enableReactDevtools: false,
frameless: false,
@ -220,6 +222,17 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
}
}
export function migratePluginSetting(pluginName: string, oldSetting: string, newSetting: string) {
const settings = SettingsStore.plain.plugins[pluginName];
if (!settings) return;
if (!Object.hasOwn(settings, oldSetting) || Object.hasOwn(settings, newSetting)) return;
settings[newSetting] = settings[oldSetting];
delete settings[oldSetting];
SettingsStore.markAsChanged();
}
export function definePluginSettings<
Def extends SettingsDefinition,
Checks extends SettingsChecks<Def>,

View file

@ -17,16 +17,22 @@
*/
import { Button } from "@webpack/common";
import { ButtonProps } from "@webpack/types";
import { Heart } from "./Heart";
export default function DonateButton(props: any) {
export default function DonateButton({
look = Button.Looks.LINK,
color = Button.Colors.TRANSPARENT,
...props
}: Partial<ButtonProps>) {
return (
<Button
{...props}
look={Button.Looks.LINK}
color={Button.Colors.TRANSPARENT}
look={look}
color={color}
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/Vendicated")}
innerClassName="vc-donate-button"
>
<Heart />
Donate

View file

@ -16,14 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export function Heart() {
import { classes } from "@utils/misc";
import { SVGProps } from "react";
export function Heart(props: SVGProps<SVGSVGElement>) {
return (
<svg
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
height="16"
width="16"
style={{ marginRight: "0.5em", transform: "translateY(2px)" }}
{...props}
className={classes("vc-heart-icon", props.className)}
>
<path
fill="#db61a2"

View file

@ -37,6 +37,7 @@ import { Constructor } from "type-fest";
import { PluginMeta } from "~plugins";
import {
ISettingCustomElementProps,
ISettingElementProps,
SettingBooleanComponent,
SettingCustomComponent,
@ -74,7 +75,7 @@ function makeDummyUser(user: { username: string; id?: string; avatar?: string; }
return newUser;
}
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any>>> = {
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any> | ISettingCustomElementProps<any>>> = {
[OptionType.STRING]: SettingTextComponent,
[OptionType.NUMBER]: SettingNumericComponent,
[OptionType.BIGINT]: SettingNumericComponent,

View file

@ -18,8 +18,8 @@
import { PluginOptionComponent } from "@utils/types";
import { ISettingElementProps } from ".";
import { ISettingCustomElementProps } from ".";
export function SettingCustomComponent({ option, onChange, onError }: ISettingElementProps<PluginOptionComponent>) {
export function SettingCustomComponent({ option, onChange, onError }: ISettingCustomElementProps<PluginOptionComponent>) {
return option.component({ setValue: onChange, setError: onError, option });
}

View file

@ -51,6 +51,7 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
onChange={handleChange}
placeholder={option.placeholder ?? "Enter a value"}
disabled={option.disabled?.call(definedSettings) ?? false}
maxLength={null}
{...option.componentProps}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}

View file

@ -18,7 +18,7 @@
import { DefinedSettings, PluginOptionBase } from "@utils/types";
export interface ISettingElementProps<T extends PluginOptionBase> {
interface ISettingElementPropsBase<T> {
option: T;
onChange(newValue: any): void;
pluginSettings: {
@ -30,6 +30,9 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
definedSettings?: DefinedSettings;
}
export type ISettingElementProps<T extends PluginOptionBase> = ISettingElementPropsBase<T>;
export type ISettingCustomElementProps<T extends Omit<PluginOptionBase, "description" | "placeholder">> = ISettingElementPropsBase<T>;
export * from "../../Badge";
export * from "./SettingBooleanComponent";
export * from "./SettingCustomComponent";

View file

@ -69,7 +69,7 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
<Forms.FormText className={cl("dep-text")}>
Restart now to apply new plugins and their settings
</Forms.FormText>
<Button onClick={() => location.reload()}>
<Button onClick={() => location.reload()} className={cl("restart-button")}>
Restart
</Button>
</>
@ -158,8 +158,8 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
className={classes(ButtonClasses.button, cl("info-button"))}
>
{plugin.options && !isObjectEmpty(plugin.options)
? <CogWheel />
: <InfoIcon />}
? <CogWheel className={cl("info-icon")} />
: <InfoIcon className={cl("info-icon")} />}
</button>
}
/>
@ -177,10 +177,10 @@ function ExcludedPluginsList({ search }: { search: string; }) {
const matchingExcludedPlugins = Object.entries(ExcludedPlugins)
.filter(([name]) => name.toLowerCase().includes(search));
const ExcludedReasons: Record<"web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev", string> = {
const ExcludedReasons: Record<"web" | "discordDesktop" | "vesktop" | "desktop" | "dev", string> = {
desktop: "Discord Desktop app or Vesktop",
discordDesktop: "Discord Desktop app",
vencordDesktop: "Vesktop app",
vesktop: "Vesktop app",
web: "Vesktop app and the Web version of Discord",
dev: "Developer version of Vencord"
};

View file

@ -63,10 +63,7 @@
height: 8em;
display: flex;
flex-direction: column;
}
.vc-plugins-info-card div {
line-height: 32px;
gap: 0.25em;
}
.vc-plugins-restart-card {
@ -76,11 +73,11 @@
color: var(--info-warning-text);
}
.vc-plugins-restart-card button {
.vc-plugins-restart-button {
margin-top: 0.5em;
background: var(--info-warning-foreground) !important;
}
.vc-plugins-info-button svg:not(:hover, :focus) {
.vc-plugins-info-icon:not(:hover, :focus) {
color: var(--text-muted);
}

View file

@ -27,7 +27,7 @@ interface SwitchProps {
disabled?: boolean;
}
const SWITCH_ON = "var(--green-360)";
const SWITCH_ON = "var(--brand-500)";
const SWITCH_OFF = "var(--primary-400)";
const SwitchClasses = findByPropsLazy("slider", "input", "container");

View file

@ -65,7 +65,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
}
const canonicalMatch = canonicalizeMatch(new RegExp(match));
try {
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
const canonicalReplace = canonicalizeReplace(replacement, 'Vencord.Plugins.plugins["YourPlugin"]');
var patched = src.replace(canonicalMatch, canonicalReplace as string);
setReplacementError(void 0);
} catch (e) {

View file

@ -0,0 +1,77 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./specialCard.css";
import { classNameFactory } from "@api/Styles";
import { Card, Clickable, Forms, React } from "@webpack/common";
import type { PropsWithChildren } from "react";
const cl = classNameFactory("vc-special-");
interface StyledCardProps {
title: string;
subtitle?: string;
description: string;
cardImage?: string;
backgroundImage?: string;
backgroundColor?: string;
buttonTitle?: string;
buttonOnClick?: () => void;
}
export function SpecialCard({ title, subtitle, description, cardImage, backgroundImage, backgroundColor, buttonTitle, buttonOnClick: onClick, children }: PropsWithChildren<StyledCardProps>) {
const cardStyle: React.CSSProperties = {
backgroundColor: backgroundColor || "#9c85ef",
backgroundImage: `url(${backgroundImage || ""})`,
};
return (
<Card className={cl("card", "card-special")} style={cardStyle}>
<div className={cl("card-flex")}>
<div className={cl("card-flex-main")}>
<Forms.FormTitle className={cl("title")} tag="h5">{title}</Forms.FormTitle>
<Forms.FormText className={cl("subtitle")}>{subtitle}</Forms.FormText>
<Forms.FormText className={cl("text")}>{description}</Forms.FormText>
{children}
</div>
{cardImage && (
<div className={cl("image-container")}>
<img
role="presentation"
src={cardImage}
alt=""
className={cl("image")}
/>
</div>
)}
</div>
{buttonTitle && (
<>
<Forms.FormDivider className={cl("seperator")} />
<Clickable onClick={onClick} className={cl("hyperlink")}>
<Forms.FormText className={cl("hyperlink-text")}>
{buttonTitle}
</Forms.FormText>
</Clickable>
</>
)}
</Card>
);
}

View file

@ -20,29 +20,38 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { gitRemote } from "@shared/vencordUserAgent";
import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
import { identity, isPluginDev } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { Button, Card, Forms, React, Select, Switch } from "@webpack/common";
import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common";
import BadgeAPI from "../../plugins/_api/badges";
import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "..";
import { openNotificationSettingsModal } from "./NotificationSettings";
import { QuickAction, QuickActionCard } from "./quickActions";
import { SettingsTab, wrapTab } from "./shared";
import { SpecialCard } from "./SpecialCard";
const cl = classNameFactory("vc-settings-");
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png";
const VENNIE_DONATOR_IMAGE = "https://cdn.discordapp.com/emojis/1238120638020063377.png";
const COZY_CONTRIB_IMAGE = "https://cdn.discordapp.com/emojis/1026533070955872337.png";
const DONOR_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070116305436712.png?size=2048";
const CONTRIB_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070166481895484.png?size=2048";
type KeysOfType<Object, Type> = {
[K in keyof Object]: Object[K] extends Type ? K : never;
}[keyof Object];
function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
fallbackValue: "Loading..."
@ -55,6 +64,8 @@ function VencordSettings() {
const isMac = navigator.platform.toLowerCase().startsWith("mac");
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
const user = UserStore.getCurrentUser();
const Switches: Array<false | {
key: KeysOfType<typeof settings, boolean>;
title: string;
@ -99,7 +110,44 @@ function VencordSettings() {
return (
<SettingsTab title="Vencord Settings">
<DonateCard image={donateImage} />
{isDonor(user?.id)
? (
<SpecialCard
title="Donations"
subtitle="Thank you for donating!"
description="You can manage your perks at any time by messaging @vending.machine."
cardImage={VENNIE_DONATOR_IMAGE}
backgroundImage={DONOR_BACKGROUND_IMAGE}
backgroundColor="#ED87A9"
>
<DonateButtonComponent />
</SpecialCard>
)
: (
<SpecialCard
title="Support the Project"
description="Please consider supporting the development of Vencord by donating!"
cardImage={donateImage}
backgroundImage={DONOR_BACKGROUND_IMAGE}
backgroundColor="#c3a3ce"
>
<DonateButtonComponent />
</SpecialCard>
)
}
{isPluginDev(user?.id) && (
<SpecialCard
title="Contributions"
subtitle="Thank you for contributing!"
description="Since you've contributed to Vencord you now have a cool new badge!"
cardImage={COZY_CONTRIB_IMAGE}
backgroundImage={CONTRIB_BACKGROUND_IMAGE}
backgroundColor="#EDCC87"
buttonTitle="See what you've contributed to"
buttonOnClick={() => openContributorModal(user)}
/>
)}
<Forms.FormSection title="Quick Actions">
<QuickActionCard>
<QuickAction
@ -239,31 +287,19 @@ function VencordSettings() {
);
}
interface DonateCardProps {
image: string;
}
function DonateCard({ image }: DonateCardProps) {
function DonateButtonComponent() {
return (
<Card className={cl("card", "donate")}>
<div>
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
<Forms.FormText>Please consider supporting the development of Vencord by donating!</Forms.FormText>
<DonateButton style={{ transform: "translateX(-1em)" }} />
</div>
<img
role="presentation"
src={image}
alt=""
height={128}
style={{
imageRendering: image === SHIGGY_DONATE_IMAGE ? "pixelated" : void 0,
marginLeft: "auto",
transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : void 0
}}
/>
</Card>
<DonateButton
look={Button.Looks.FILLED}
color={Button.Colors.WHITE}
style={{ marginTop: "1em" }}
/>
);
}
function isDonor(userId: string): boolean {
const donorBadges = BadgeAPI.getDonorBadges(userId);
return GuildMemberStore.getMember(VENCORD_GUILD_ID, userId)?.roles.includes(DONOR_ROLE_ID) || !!donorBadges;
}
export default wrapTab(VencordSettings, "Vencord Settings");

View file

@ -11,6 +11,11 @@
box-sizing: border-box;
}
.visual-refresh .vc-addon-card {
background-color: var(--card-primary-bg);
border: 1px solid var(--border-subtle);
}
.vc-addon-card-disabled {
opacity: 0.6;
}
@ -21,6 +26,11 @@
box-shadow: var(--elevation-high);
}
.visual-refresh .vc-addon-card:hover {
/* same as non-hover, here to overwrite the non-refresh hover background */
background-color: var(--card-primary-bg);
}
.vc-addon-header {
margin-top: auto;
display: flex;

View file

@ -1,12 +1,17 @@
.vc-settings-quickActions-card {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, max-content));
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
justify-content: center;
padding: 0.5em 0;
padding: 0.5em;
margin-bottom: 1em;
}
@media (width <=1040px) {
.vc-settings-quickActions-card {
grid-template-columns: repeat(2, 1fr);
}
}
.vc-settings-quickActions-pill {
all: unset;
background: var(--background-secondary);
@ -14,12 +19,16 @@
display: flex;
align-items: center;
gap: 0.5em;
padding: 8px 12px;
border-radius: 9999px;
padding: 8px 9px;
border-radius: 8px;
transition: 0.1s ease-out;
box-sizing: border-box;
}
.vc-settings-quickActions-pill:hover {
background: var(--background-secondary-alt);
transform: translateY(-1px);
box-shadow: var(--elevation-high);
}
.vc-settings-quickActions-pill:focus-visible {
@ -27,7 +36,15 @@
outline-offset: 2px;
}
.visual-refresh .vc-settings-quickActions-pill {
background: var(--button-secondary-background);
}
.visual-refresh .vc-settings-quickActions-pill:hover {
background: var(--button-secondary-background-hover);
}
.vc-settings-quickActions-img {
width: 24px;
height: 24px;
}
}

View file

@ -0,0 +1,92 @@
.vc-donate-button {
overflow: visible !important;
}
.vc-donate-button .vc-heart-icon {
transition: transform 0.3s;
}
.vc-donate-button:hover .vc-heart-icon {
transform: scale(1.1);
z-index: 10;
position: relative;
}
.vc-settings-card {
padding: 1em;
margin-bottom: 1em;
}
.vc-special-card-special {
padding: 1em 1.5em;
margin-bottom: 1em;
background-size: cover;
background-position: center;
}
.vc-special-card-flex {
display: flex;
flex-direction: row;
}
.vc-special-card-flex-main {
width: 100%;
}
.vc-special-title {
color: black;
}
.vc-special-subtitle {
color: black;
font-size: 1.2em;
font-weight: bold;
margin-top: 0.5em;
}
.vc-special-text {
color: black;
font-size: 1em;
margin-top: .75em;
white-space: pre-line;
}
.vc-special-seperator {
margin-top: .75em;
border-top: 1px solid white;
opacity: 0.4;
}
.vc-special-hyperlink {
margin-top: 1em;
cursor: pointer;
.vc-special-hyperlink-text {
color: black;
font-size: 1em;
font-weight: bold;
text-align: center;
transition: text-decoration 0.5s;
cursor: pointer;
}
&:hover .vc-special-hyperlink-text {
text-decoration: underline;
}
}
.vc-special-image-container {
display: flex;
justify-content: center;
align-items: center;
margin-left: 1em;
flex-shrink: 0;
width: 100px;
height: 100px;
border-radius: 50%;
background-color: white;
}
.vc-special-image {
width: 65%;
}

View file

@ -5,3 +5,8 @@
.vc-owner-crown-icon {
color: var(--text-warning);
}
.vc-heart-icon {
margin-right: 0.5em;
translate: 0 2px;
}

View file

@ -23,35 +23,61 @@ if (IS_DEV || IS_REPORTER) {
var logger = new Logger("Tracer", "#FFD166");
}
const noop = function () { };
export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
export const beginTrace = !(IS_DEV || IS_REPORTER) ? () => { } :
function beginTrace(name: string, ...args: any[]) {
if (name in traces)
if (name in traces) {
throw new Error(`Trace ${name} already exists!`);
}
traces[name] = [performance.now(), args];
};
export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
const end = performance.now();
export const finishTrace = !(IS_DEV || IS_REPORTER) ? () => 0 :
function finishTrace(name: string) {
const end = performance.now();
const [start, args] = traces[name];
delete traces[name];
const [start, args] = traces[name];
delete traces[name];
logger.debug(`${name} took ${end - start}ms`, args);
};
const totalTime = end - start;
logger.debug(`${name} took ${totalTime}ms`, args);
return totalTime;
};
type Func = (...args: any[]) => any;
type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
const noopTracer =
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
function noopTracerWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {
return function (this: unknown, ...args: Parameters<F>): [ReturnType<F>, number] {
return [f.apply(this, args), 0];
};
}
function noopTracer<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {
return f;
}
export const traceFunctionWithResults = !(IS_DEV || IS_REPORTER)
? noopTracerWithResults
: function traceFunctionWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): (this: unknown, ...args: Parameters<F>) => [ReturnType<F>, number] {
return function (this: unknown, ...args: Parameters<F>) {
const traceName = mapper?.(...args) ?? name;
beginTrace(traceName, ...arguments);
try {
return [f.apply(this, args), finishTrace(traceName)];
} catch (e) {
finishTrace(traceName);
throw e;
}
};
};
export const traceFunction = !(IS_DEV || IS_REPORTER)
? noopTracer
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
return function (this: any, ...args: Parameters<F>) {
return function (this: unknown, ...args: Parameters<F>) {
const traceName = mapper?.(...args) ?? name;
beginTrace(traceName, ...arguments);

View file

@ -8,23 +8,26 @@ import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches";
import * as Webpack from "@webpack";
import { wreq } from "@webpack";
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
import { AnyModuleFactory, ModuleFactory } from "@webpack/wreq.d";
export async function loadLazyChunks() {
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
try {
LazyChunkLoaderLogger.log("Loading all chunks...");
const validChunks = new Set<number>();
const invalidChunks = new Set<number>();
const deferredRequires = new Set<number>();
const validChunks = new Set<PropertyKey>();
const invalidChunks = new Set<PropertyKey>();
const deferredRequires = new Set<PropertyKey>();
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
const { promise: chunksSearchingDone, resolve: chunksSearchingResolve } = Promise.withResolvers<void>();
// True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>;
/* This regex loads all language packs which makes webpack finds testing extremely slow, so for now, lets use one which doesnt include those
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g);
*/
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
let foundCssDebuggingLoad = false;
@ -34,12 +37,15 @@ export async function loadLazyChunks() {
const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>();
const validChunkGroups = new Set<[chunkIds: PropertyKey[], entryPoint: PropertyKey]>();
const shouldForceDefer = false;
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : [];
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => {
const numChunkId = Number(m[1]);
return Number.isNaN(numChunkId) ? m[1] : numChunkId;
}) : [];
if (chunkIds.length === 0) {
return;
@ -62,7 +68,7 @@ export async function loadLazyChunks() {
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes("importScripts("));
.then(t => /importScripts\(|self\.postMessage/.test(t));
if (isWorkerAsset) {
invalidChunks.add(id);
@ -74,7 +80,8 @@ export async function loadLazyChunks() {
}
if (!invalidChunkGroup) {
validChunkGroups.add([chunkIds, Number(entryPoint)]);
const numEntryPoint = Number(entryPoint);
validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : numEntryPoint]);
}
}));
@ -82,7 +89,7 @@ export async function loadLazyChunks() {
await Promise.all(
Array.from(validChunkGroups)
.map(([chunkIds]) =>
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
Promise.all(chunkIds.map(id => wreq.e(id)))
)
);
@ -94,7 +101,7 @@ export async function loadLazyChunks() {
continue;
}
if (wreq.m[entryPoint]) wreq(entryPoint as any);
if (wreq.m[entryPoint]) wreq(entryPoint);
} catch (err) {
console.error(err);
}
@ -122,41 +129,44 @@ export async function loadLazyChunks() {
}, 0);
}
Webpack.factoryListeners.add(factory => {
function factoryListener(factory: AnyModuleFactory | ModuleFactory) {
let isResolved = false;
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
searchAndLoadLazyChunks(String(factory))
.then(() => isResolved = true)
.catch(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
});
}
for (const factoryId in wreq.m) {
let isResolved = false;
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
Webpack.factoryListeners.add(factoryListener);
for (const moduleId in wreq.m) {
factoryListener(wreq.m[moduleId]);
}
await chunksSearchingDone;
Webpack.factoryListeners.delete(factoryListener);
// Require deferred entry points
for (const deferredRequire of deferredRequires) {
wreq!(deferredRequire as any);
wreq(deferredRequire);
}
// All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as number[];
const allChunks = [] as PropertyKey[];
// Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
for (const currentMatch of String(wreq.u).matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;
allChunks.push(Number(id));
const numId = Number(id);
allChunks.push(Number.isNaN(numId) ? id : numId);
}
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
// Chunks that are not loaded (not used) by Discord code anymore
// Chunks which our regex could not catch to load
// It will always contain WebWorker assets, and also currently contains some language packs which are loaded differently
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});
@ -164,14 +174,11 @@ export async function loadLazyChunks() {
await Promise.all(chunksLeft.map(async id => {
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes("importScripts("));
.then(t => /importScripts\(|self\.postMessage/.test(t));
// Loads and requires a chunk
// Loads the chunk. Currently this only happens with the language packs which are loaded differently
if (!isWorkerAsset) {
await wreq.e(id as any);
// Technically, the id of the chunk does not match the entry point
// But, still try it because we have no way to get the actual entry point
if (wreq.m[id]) wreq(id as any);
await wreq.e(id);
}
}));

View file

@ -6,28 +6,56 @@
import { Logger } from "@utils/Logger";
import * as Webpack from "@webpack";
import { patches } from "plugins";
import { getBuildNumber, patchTimings } from "@webpack/patcher";
import { addPatch, patches } from "../plugins";
import { loadLazyChunks } from "./loadLazyChunks";
const ReporterLogger = new Logger("Reporter");
async function runReporter() {
const ReporterLogger = new Logger("Reporter");
try {
ReporterLogger.log("Starting test...");
let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
const { promise: loadLazyChunksDone, resolve: loadLazyChunksResolve } = Promise.withResolvers<void>();
// The main patch for starting the reporter chunk loading
addPatch({
find: '"Could not find app-mount"',
replacement: {
match: /(?<="use strict";)/,
replace: "Vencord.Webpack._initReporter();"
}
}, "Vencord Reporter");
// @ts-ignore
Vencord.Webpack._initReporter = function () {
// initReporter is called in the patched entry point of Discord
// setImmediate to only start searching for lazy chunks after Discord initialized the app
setTimeout(() => loadLazyChunks().then(loadLazyChunksResolve), 0);
};
Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
await loadLazyChunksDone;
if (IS_REPORTER && IS_WEB && !IS_VESKTOP) {
console.log("[REPORTER_META]", {
buildNumber: getBuildNumber(),
buildHash: window.GLOBAL_ENV.SENTRY_TAGS.buildId
});
}
for (const patch of patches) {
if (!patch.all) {
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
}
}
for (const [plugin, moduleId, match, totalTime] of patchTimings) {
if (totalTime > 5) {
new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
}
}
for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
let method = searchType;
@ -50,9 +78,9 @@ async function runReporter() {
result = await Webpack.extractAndLoadChunks(code, matcher);
if (result === false) result = null;
} else if (method === "mapMangledModule") {
const [code, mapper] = args;
const [code, mapper, includeBlacklistedExports] = args;
result = Webpack.mapMangledModule(code, mapper);
result = Webpack.mapMangledModule(code, mapper, includeBlacklistedExports);
if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail");
} else {
// @ts-ignore
@ -62,14 +90,21 @@ async function runReporter() {
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail");
} catch (e) {
let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
else if (method === "mapMangledModule") {
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
if (args[0].$$vencordProps != null) {
logMessage += `(${args[0].$$vencordProps.map(arg => `"${arg}"`).join(", ")})`;
} else {
logMessage += `(${args[0].toString().slice(0, 147)}...)`;
}
} else if (method === "extractAndLoadChunks") {
logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
} else if (method === "mapMangledModule") {
const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null);
logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(",\n")}\n})`;
} else {
logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
}
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
ReporterLogger.log("Webpack Find Fail:", logMessage);
}
@ -81,4 +116,6 @@ async function runReporter() {
}
}
runReporter();
// Run after the Vencord object has been created.
// We need to add extra properties to it, and it is only created after all of Vencord code has ran
setTimeout(runReporter, 0);

7
src/globals.d.ts vendored
View file

@ -64,13 +64,8 @@ declare global {
export var Vesktop: any;
export var VesktopNative: any;
interface Window {
webpackChunkdiscord_app: {
push(chunk: any): any;
pop(): any;
};
interface Window extends Record<PropertyKey, any> {
_: LoDashStatic;
[k: string]: any;
}
}

2
src/modules.d.ts vendored
View file

@ -25,7 +25,7 @@ declare module "~plugins" {
folderName: string;
userPlugin: boolean;
}>;
export const ExcludedPlugins: Record<string, "web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev">;
export const ExcludedPlugins: Record<string, "web" | "discordDesktop" | "vesktop" | "desktop" | "dev">;
}
declare module "~pluginNatives" {

View file

@ -28,7 +28,7 @@ import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc";
import { closeModal, Modals, openModal } from "@utils/modal";
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Forms, Toasts, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
@ -65,27 +65,25 @@ export default definePlugin({
{
find: ".FULL_SIZE]:26",
replacement: {
match: /(?<=(\i)=\(0,\i\.\i\)\(\i\);)return 0===\i.length\?/,
replace: "$1.unshift(...$self.getBadges(arguments[0].displayProfile));$&"
match: /(?=;return 0===(\i)\.length\?)(?<=(\i)\.useMemo.+?)/,
replace: ";$1=$2.useMemo(()=>[...$self.getBadges(arguments[0].displayProfile),...$1],[$1])"
}
},
{
find: ".description,delay:",
find: "#{intl::PROFILE_USER_BADGES}",
replacement: [
{
// alt: "", aria-hidden: false, src: originalSrc
match: /alt:" ","aria-hidden":!0,src:(?=.{0,20}(\i)\.icon)/,
// ...badge.props, ..., src: badge.image ?? ...
replace: "...$1.props,$& $1.image??"
match: /(alt:" ","aria-hidden":!0,src:)(.+?)(?=,)(?<=href:(\i)\.link.+?)/,
replace: (_, rest, originalSrc, badge) => `...${badge}.props,${rest}${badge}.image??(${originalSrc})`
},
{
match: /(?<="aria-label":(\i)\.description,.{0,200})children:/,
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
replace: "children:$1.component?$self.renderBadgeComponent({...$1}) :"
},
// conditionally override their onClick with badge.onClick if it exists
{
match: /href:(\i)\.link/,
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, $1) }),$&"
replace: "...($1.onClick&&{onClick:vcE=>$1.onClick(vcE,$1)}),$&"
}
]
}
@ -144,8 +142,8 @@ export default definePlugin({
closeModal(modalKey);
VencordNative.native.openExternal("https://github.com/sponsors/Vendicated");
}}>
<Modals.ModalRoot {...props}>
<Modals.ModalHeader>
<ModalRoot {...props}>
<ModalHeader>
<Flex style={{ width: "100%", justifyContent: "center" }}>
<Forms.FormTitle
tag="h2"
@ -159,8 +157,8 @@ export default definePlugin({
Vencord Donor
</Forms.FormTitle>
</Flex>
</Modals.ModalHeader>
<Modals.ModalContent>
</ModalHeader>
<ModalContent>
<Flex>
<img
role="presentation"
@ -183,13 +181,13 @@ export default definePlugin({
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
</Forms.FormText>
</div>
</Modals.ModalContent>
<Modals.ModalFooter>
</ModalContent>
<ModalFooter>
<Flex style={{ width: "100%", justifyContent: "center" }}>
<DonateButton />
</Flex>
</Modals.ModalFooter>
</Modals.ModalRoot>
</ModalFooter>
</ModalRoot>
</ErrorBoundary>
));
},

View file

@ -12,11 +12,16 @@ export default definePlugin({
description: "API to add buttons to the chat input",
authors: [Devs.Ven],
patches: [{
find: '"sticker")',
replacement: {
match: /return\(!\i\.\i&&(?=\(\i\.isDM.+?(\i)\.push\(.{0,50}"gift")/,
replace: "$&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)&&"
patches: [
{
find: '"sticker")',
replacement: {
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
match: /return\((!)?\i\.\i(?:\|\||&&)(?=\(.+?(\i)\.push)/,
replace: (m, not, children) => not
? `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),true)&&`
: `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),false)||`
}
}
}]
]
});

View file

@ -34,12 +34,22 @@ export default definePlugin({
}
},
{
find: ".Menu,{",
find: "navId:",
all: true,
replacement: {
match: /Menu,{(?<=\.jsxs?\)\(\i\.Menu,{)/g,
replace: "$&contextMenuApiArguments:typeof arguments!=='undefined'?arguments:[],"
}
noWarn: true,
replacement: [
{
match: /navId:(?=.+?([,}].*?\)))/g,
replace: (m, rest) => {
// Check if this navId: match is a destructuring statement, ignore it if it is
const destructuringMatch = rest.match(/}=.+/);
if (destructuringMatch == null) {
return `contextMenuAPIArguments:typeof arguments!=='undefined'?arguments:[],${m}`;
}
return m;
}
}
]
}
]
});

View file

@ -14,10 +14,17 @@ export default definePlugin({
description: "Allows you to omit either width or height when opening an image modal",
patches: [
{
find: "SCALE_DOWN:",
find: ".contain,SCALE_DOWN:",
replacement: {
match: /!\(null==(\i)\|\|0===\i\|\|null==(\i)\|\|0===\i\)/,
replace: (_, width, height) => `!((null == ${width} || 0 === ${width}) && (null == ${height} || 0 === ${height}))`
match: /(?<="IMAGE"===\i\?)\i(?=\?)/,
replace: "true"
}
},
{
find: ".dimensionlessImage,",
replacement: {
match: /(?<="IMAGE"===\i&&\(\i=)\i(?=\?)/,
replace: "true"
}
}
]

View file

@ -19,10 +19,15 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import managedStyle from "./style.css?managed";
export default definePlugin({
name: "MemberListDecoratorsAPI",
description: "API to add decorators to member list (both in servers and DMs)",
authors: [Devs.TheSun, Devs.Ven],
managedStyle,
patches: [
{
find: ".lostPermission)",
@ -32,7 +37,7 @@ export default definePlugin({
replace: "$&vencordProps=$1,"
}, {
match: /#{intl::GUILD_OWNER}(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
replace: "$&(typeof vencordProps=='undefined'?null:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
}
]
},
@ -40,8 +45,8 @@ export default definePlugin({
find: "PrivateChannel.renderAvatar",
replacement: {
match: /decorators:(\i\.isSystemDM\(\))\?(.+?):null/,
replace: "decorators:[...Vencord.Api.MemberListDecorators.__getDecorators(arguments[0]), $1?$2:null]"
replace: "decorators:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0]),$1?$2:null]"
}
}
],
]
});

View file

@ -0,0 +1,11 @@
.vc-member-list-decorators-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25em;
}
.vc-member-list-decorators-wrapper:not(:empty) {
/* Margin to match default Discord decorators */
margin-left: 0.25em;
}

View file

@ -0,0 +1,68 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin from "@utils/types";
// duplicate values have multiple branches with different types. Just include all to be safe
const nameMap = {
radio: "MenuRadioItem",
separator: "MenuSeparator",
checkbox: "MenuCheckboxItem",
groupstart: "MenuGroup",
control: "MenuControlItem",
compositecontrol: "MenuControlItem",
item: "MenuItem",
customitem: "MenuItem",
};
export default definePlugin({
name: "MenuItemDemanglerAPI",
description: "Demangles Discord's Menu Item module",
authors: [Devs.Ven],
required: true,
patches: [
{
find: '"Menu API',
replacement: {
match: /function.{0,80}type===(\i\.\i)\).{0,50}navigable:.+?Menu API/s,
replace: (m, mod) => {
const nameAssignments = [] as string[];
// if (t.type === m.MenuItem)
const typeCheckRe = canonicalizeMatch(/\(\i\.type===(\i\.\i)\)/g);
// push({type:"item"})
const pushTypeRe = /type:"(\w+)"/g;
let typeMatch: RegExpExecArray | null;
// for each if (t.type === ...)
while ((typeMatch = typeCheckRe.exec(m)) !== null) {
// extract the current menu item
const item = typeMatch[1];
// Set the starting index of the second regex to that of the first to start
// matching from after the if
pushTypeRe.lastIndex = typeCheckRe.lastIndex;
// extract the first type: "..."
const type = pushTypeRe.exec(m)?.[1];
if (type && type in nameMap) {
const name = nameMap[type];
nameAssignments.push(`Object.defineProperty(${item},"name",{value:"${name}"})`);
}
}
if (nameAssignments.length < 6) {
console.warn("[MenuItemDemanglerAPI] Expected to at least remap 6 items, only remapped", nameAssignments.length);
}
// Merge all our redefines with the actual module
return `${nameAssignments.join(";")};${m}`;
},
},
},
],
});

View file

@ -19,17 +19,22 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import managedStyle from "./style.css?managed";
export default definePlugin({
name: "MessageDecorationsAPI",
description: "API to add decorations to messages",
authors: [Devs.TheSun],
managedStyle,
patches: [
{
find: '"Message Username"',
replacement: {
match: /#{intl::GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE}.+?}\),\i(?=\])/,
replace: "$&,...Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0])"
match: /#{intl::GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE}.+?renderPopout:.+?(?=\])/,
replace: "$&,Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0])"
}
}
],
]
});

View file

@ -0,0 +1,18 @@
.vc-message-decorations-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.25em;
}
.vc-message-decorations-wrapper:not(:empty) {
/* Margin to match default Discord decorators */
margin-left: 0.25em;
/* Align vertically */
position: relative;
vertical-align: top;
top: 0.1rem;
height: calc(1rem + 4px);
max-height: calc(1rem + 4px)
}

View file

@ -37,12 +37,9 @@ export default definePlugin({
{
find: ".handleSendMessage,onResize",
replacement: {
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
match: /(\{openWarningPopout:.{0,100}type:this.props.chatInputType.+?\.then\()(\i=>\{.+?let (\i)=\i\.\i\.parse\((\i),.+?let (\i)=\i\.\i\.getSendMessageOptions\(\{.+?\}\);)(?<=\)\(({.+?})\)\.then.+?)/,
// props.chatInputType...then((async function(isMessageValid)... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply); if(await Vencord.api...) return { shoudClear:true, shouldRefocus:true };
replace: (_, rest1, rest2, parsedMessage, channel, replyOptions, extra) => "" +
`${rest1}async ${rest2}` +
// https://regex101.com/r/hBlXpl/1
match: /let (\i)=\i\.\i\.parse\((\i),.+?let (\i)=\i\.\i\.getSendMessageOptions\(\{.+?\}\);(?<=\)\(({.+?})\)\.then.+?)/,
replace: (m, parsedMessage, channel, replyOptions, extra) => m +
`if(await Vencord.Api.MessageEvents._handlePreSend(${channel}.id,${parsedMessage},${extra},${replyOptions}))` +
"return{shouldClear:false,shouldRefocus:true};"
}
@ -52,8 +49,7 @@ export default definePlugin({
replacement: {
match: /let\{id:\i}=(\i),{id:\i}=(\i);return \i\.useCallback\((\i)=>\{/,
replace: (m, message, channel, event) =>
// the message param is shadowed by the event param, so need to alias them
`const vcMsg=${message},vcChan=${channel};${m}Vencord.Api.MessageEvents._handleClick(vcMsg, vcChan, ${event});`
`const vcMsg=${message},vcChan=${channel};${m}Vencord.Api.MessageEvents._handleClick(vcMsg,vcChan,${event});`
}
}
]

View file

@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { WebpackRequire } from "@webpack/wreq.d";
const settings = definePluginSettings({
disableAnalytics: {
@ -81,9 +82,9 @@ export default definePlugin({
Object.defineProperty(Function.prototype, "g", {
configurable: true,
set(v: any) {
set(this: WebpackRequire, globalObj: WebpackRequire["g"]) {
Object.defineProperty(this, "g", {
value: v,
value: globalObj,
configurable: true,
enumerable: true,
writable: true
@ -92,11 +93,11 @@ export default definePlugin({
// Ensure this is most likely the Sentry WebpackInstance.
// Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: <g></g>) to include it
const { stack } = new Error();
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || !String(this).includes("exports:{}") || this.c != null) {
if (this.c != null || !stack?.includes("http") || !String(this).includes("exports:{}")) {
return;
}
const assetPath = stack?.match(/\/assets\/.+?\.js/)?.[0];
const assetPath = stack.match(/http.+?(?=:\d+?:\d+?$)/m)?.[0];
if (!assetPath) {
return;
}
@ -106,7 +107,8 @@ export default definePlugin({
srcRequest.send();
// Final condition to see if this is the Sentry WebpackInstance
if (!srcRequest.responseText.includes("window.DiscordSentry=")) {
// This is matching window.DiscordSentry=, but without `window` to avoid issues on some proxies
if (!srcRequest.responseText.includes(".DiscordSentry=")) {
return;
}

View file

@ -65,7 +65,8 @@ export default definePlugin({
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
},
{
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?(?:function\(\){return |\(\)=>))\2/,
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
}
]
@ -157,6 +158,9 @@ export default definePlugin({
aboveActivity: getIntlMessage("ACTIVITY_SETTINGS")
};
if (!names[settingsLocation] || names[settingsLocation].endsWith("_SETTINGS"))
return firstChild === "PREMIUM";
return header === names[settingsLocation];
} catch {
return firstChild === "PREMIUM";

View file

@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
import { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, KNOWN_ISSUES_CHANNEL_ID, REGULAR_ROLE_ID, SUPPORT_CATEGORY_ID, SUPPORT_CHANNEL_ID, VENBOT_USER_ID, VENCORD_GUILD_ID } from "@utils/constants";
import { sendMessage } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
@ -32,7 +32,8 @@ import { onlyOnce } from "@utils/onlyOnce";
import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types";
import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import { Channel } from "discord-types/general";
import { JSX } from "react";
import gitHash from "~git-hash";
@ -40,27 +41,24 @@ import plugins, { PluginMeta } from "~plugins";
import SettingsPlugin from "./settings";
const VENCORD_GUILD_ID = "1015060230222131221";
const VENBOT_USER_ID = "1017176847865352332";
const KNOWN_ISSUES_CHANNEL_ID = "1222936386626129920";
const CodeBlockRe = /```js\n(.+?)```/s;
const AllowedChannelIds = [
SUPPORT_CHANNEL_ID,
const AdditionalAllowedChannelIds = [
"1024286218801926184", // Vencord > #bot-spam
"1033680203433660458", // Vencord > #v
];
const TrustedRolesIds = [
"1026534353167208489", // contributor
"1026504932959977532", // regular
"1042507929485586532", // donor
CONTRIB_ROLE_ID, // contributor
REGULAR_ROLE_ID, // regular
DONOR_ROLE_ID, // donor
];
const AsyncFunction = async function () { }.constructor;
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
const isSupportAllowedChannel = (channel: Channel) => channel.parent_id === SUPPORT_CATEGORY_ID || AdditionalAllowedChannelIds.includes(channel.id);
async function forceUpdate() {
const outdated = await checkForUpdates();
if (outdated) {
@ -158,20 +156,21 @@ export default definePlugin({
{
name: "vencord-debug",
description: "Send Vencord debug info",
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || isSupportAllowedChannel(ctx.channel),
execute: async () => ({ content: await generateDebugInfoMessage() })
},
{
name: "vencord-plugins",
description: "Send Vencord plugin list",
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || isSupportAllowedChannel(ctx.channel),
execute: () => ({ content: generatePluginList() })
}
],
flux: {
async CHANNEL_SELECT({ channelId }) {
if (channelId !== SUPPORT_CHANNEL_ID) return;
const isSupportChannel = channelId === SUPPORT_CHANNEL_ID || ChannelStore.getChannel(channelId)?.parent_id === SUPPORT_CATEGORY_ID;
if (!isSupportChannel) return;
const selfId = UserStore.getCurrentUser()?.id;
if (!selfId || isPluginDev(selfId)) return;
@ -242,7 +241,7 @@ export default definePlugin({
!IS_UPDATER_DISABLED
&& (
(props.channel.id === KNOWN_ISSUES_CHANNEL_ID) ||
(props.channel.id === SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID)
(props.channel.parent_id === SUPPORT_CATEGORY_ID && props.message.author.id === VENBOT_USER_ID)
)
&& props.message.content?.includes("update");
@ -268,7 +267,7 @@ export default definePlugin({
);
}
if (props.channel.id === SUPPORT_CHANNEL_ID) {
if (props.channel.parent_id === SUPPORT_CATEGORY_ID && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel)) {
if (props.message.content.includes("/vencord-debug") || props.message.content.includes("/vencord-plugins")) {
buttons.push(
<Button

View file

@ -73,19 +73,19 @@ export default definePlugin({
group: true,
replacement: [
{
match: /(?<=\.AVATAR_SIZE\);)/,
replace: "$self.useAccountPanelRef();"
match: /let{speaking:\i/,
replace: "$self.useAccountPanelRef();$&"
},
{
match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/,
replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalRenderPopout:()=>{${originalPopout}}})`
},
{
match: /\.AVATAR,children:.+?(?=renderPopout:)/,
replace: "$&onRequestClose:$self.onPopoutClose,"
match: /\.AVATAR,children:.+?onRequestClose:\(\)=>\{/,
replace: "$&$self.onPopoutClose();"
},
{
match: /(?<=\.avatarWrapper,)/,
match: /(?<=#{intl::SET_STATUS}\),)/,
replace: "ref:$self.accountPanelRef,onContextMenu:$self.openAccountPanelContextMenu,"
}
]

View file

@ -43,16 +43,16 @@ export default definePlugin({
// Status emojis
find: "#{intl::GUILD_OWNER}),children:",
replacement: {
match: /(?<=\.activityEmoji,.+?animate:)\i/,
replace: "!0"
match: /(\.CUSTOM_STATUS.+?animate:)\i/,
replace: "$1!0"
}
},
{
// Guild Banner
find: ".animatedBannerHoverLayer,onMouseEnter:",
replacement: {
match: /(?<=guildBanner:\i,animate:)\i(?=}\))/,
replace: "!0"
match: /(\.headerContent.+?guildBanner:\i,animate:)\i/,
replace: "$1!0"
}
}
]

View file

@ -23,7 +23,7 @@ import definePlugin, { ReporterTestable } from "@utils/types";
import { findByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
const fetchApplicationsRPC = findByCodeLazy("APPLICATION_RPC(", "Client ID");
const fetchApplicationsRPC = findByCodeLazy('"Invalid Origin"', ".application");
async function lookupAsset(applicationId: string, key: string): Promise<string> {
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];

View file

@ -17,14 +17,13 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { useStateFromStores } from "@webpack/common";
import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Animations, useStateFromStores } from "@webpack/common";
import type { CSSProperties } from "react";
import { ExpandedGuildFolderStore, settings } from ".";
const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
const Animations = findByPropsLazy("a", "animated", "useTransition");
const GuildsBar = findComponentByCodeLazy('("guildsnav")');
export default ErrorBoundary.wrap(guildsBarProps => {
@ -46,7 +45,8 @@ export default ErrorBoundary.wrap(guildsBarProps => {
// Also display flex otherwise to fix scrolling
const barStyle = {
display: isFullscreen ? "none" : "flex",
} as CSSProperties;
gridArea: "betterFoldersSidebar"
} satisfies CSSProperties;
if (!guilds || !settings.store.sidebarAnim) {
return visible

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./sidebarFix.css";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
@ -173,8 +175,8 @@ export default definePlugin({
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
{
predicate: () => !settings.store.keepIcons,
match: /(?<=#{intl::SERVER_FOLDER_PLACEHOLDER}.+?useTransition\)\()/,
replace: "$self.shouldShowTransition(arguments[0])&&"
match: /(?=,\{from:\{height)/,
replace: "&&$self.shouldShowTransition(arguments[0])"
},
// If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
{
@ -185,25 +187,44 @@ export default definePlugin({
{
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
match: /(?<=\.isExpanded\),children:\[)/,
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
match: /\.isExpanded\),.{0,30}children:\[/,
replace: "$&$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
},
{
// Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
match: /(?<=\.expandedFolderBackground.+?}\),)(?=\i,)/,
replace: "!$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)?null:"
},
{
// Discord adds a slight bottom margin of 4px when it's expanded
// Which looks off when there's nothing open in the folder
predicate: () => !settings.store.keepIcons,
match: /(?=className:.{0,50}folderIcon)/,
replace: "style:arguments[0]?.isBetterFolders?{}:{marginBottom:0},"
}
]
},
{
find: "APPLICATION_LIBRARY,render:",
predicate: () => settings.store.sidebar,
replacement: {
// Render the Better Folders sidebar
match: /(container.{0,50}({className:\i\.guilds,themeOverride:\i})\))/,
replace: "$1,$self.FolderSideBar({...$2})"
}
group: true,
replacement: [
{
// Render the Better Folders sidebar
// Discord has two different places where they render the sidebar.
// One is for visual refresh, one is not,
// and each has a bunch of conditions &&ed in front of it.
// Add the betterFolders sidebar to both, keeping the conditions Discord uses.
match: /(?<=[[,])((?:!?\i&&)+)\(.{0,50}({className:\i\.guilds,themeOverride:\i})\)/g,
replace: (m, conditions, props) => `${m},${conditions}$self.FolderSideBar(${props})`
},
{
// Add grid styles to fix aligment with other visual refresh elements
match: /(?<=className:)(\i\.base)(?=,)/,
replace: "`${$self.gridStyle} ${$1}`"
}
]
},
{
find: "#{intl::DISCODO_DISABLED}",
@ -257,6 +278,8 @@ export default definePlugin({
}
},
gridStyle: "vc-betterFolders-sidebar-grid",
getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set<any>) {
return useMemo(() => {
if (!isBetterFolders || expandedFolderIds == null) return originalTree;

View file

@ -0,0 +1,9 @@
/* These area names need to be hardcoded. Only betterFoldersSidebar is added by the plugin. */
.visual-refresh .vc-betterFolders-sidebar-grid {
grid-template-columns: [start] min-content [guildsEnd] min-content [sidebarEnd] min-content [channelsEnd] 1fr [end]; /* stylelint-disable-line value-keyword-case */
grid-template-areas:
"titleBar titleBar titleBar titleBar"
"guildsList betterFoldersSidebar notice notice"
"guildsList betterFoldersSidebar channelsList page";
}

View file

@ -83,7 +83,7 @@ export default definePlugin({
if (!role) return;
if (role.colorString) {
children.push(
children.unshift(
<Menu.MenuItem
id="vc-copy-role-color"
label="Copy Role Color"
@ -93,6 +93,20 @@ export default definePlugin({
);
}
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.unshift(
<Menu.MenuItem
id="vc-edit-role"
label="Edit Role"
action={async () => {
await GuildSettingsActions.open(guild.id, "ROLES");
GuildSettingsActions.selectRole(id);
}}
icon={PencilIcon}
/>
);
}
if (role.icon) {
children.push(
<Menu.MenuItem
@ -110,20 +124,6 @@ export default definePlugin({
);
}
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.push(
<Menu.MenuItem
id="vc-edit-role"
label="Edit Role"
action={async () => {
await GuildSettingsActions.open(guild.id, "ROLES");
GuildSettingsActions.selectRole(id);
}}
icon={PencilIcon}
/>
);
}
}
}
});

View file

@ -21,7 +21,7 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
import { RenameButton } from "./components/RenameButton";
@ -34,7 +34,7 @@ const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open");
const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer");
const SessionIconClasses = findByPropsLazy("sessionIcon");
const BlobMask = findExportedComponentLazy("BlobMask");
const BlobMask = findComponentByCodeLazy("!1,lowerBadgeSize:");
const settings = definePluginSettings({
backgroundCheck: {

View file

@ -101,8 +101,8 @@ export default definePlugin({
find: 'minimal:"contentColumnMinimal"',
replacement: [
{
match: /\(0,\i\.useTransition\)\((\i)/,
replace: "(_cb=>_cb(void 0,$1))||$&"
match: /(?=\(0,\i\.\i\)\((\i),\{from:\{position:"absolute")/,
replace: "(_cb=>_cb(void 0,$1))||"
},
{
match: /\i\.animated\.div/,
@ -139,11 +139,12 @@ export default definePlugin({
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
// without possibly also catching unrelated errors of children.
//
// Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
// try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
// not in children
// Thus, we sanity check webpack modules
Layer(props: LayerProps) {
if (!FocusLock || !ComponentDispatch || !Classes) {
try {
// @ts-ignore
[FocusLock.$$vencordInternal(), ComponentDispatch, Classes].forEach(e => e.test);
} catch {
new Logger("BetterSettings").error("Failed to find some components");
return props.children;
}

View file

@ -26,10 +26,18 @@ export default definePlugin({
patches: [
{
find: '"ChannelAttachButton"',
replacement: {
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,30}?\.\.\.(\i),/,
replace: "$&onClick:$1,onContextMenu:$2.onClick,",
},
replacement: [
{
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,30}?\.\.\.(\i),/,
replace: "$&onClick:$1,onContextMenu:$2.onClick,",
noWarn: true
},
{
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,100}\},(\i)\).{0,100}children:\i/,
replace: "$&,onClick:$1,onContextMenu:$2.onClick,",
},
]
},
],
});

View file

@ -24,14 +24,14 @@ let style: HTMLStyleElement;
function setCss() {
style.textContent = `
.vc-nsfw-img [class^=imageWrapper] img,
.vc-nsfw-img [class^=wrapperPaused] video {
.vc-nsfw-img [class^=imageContainer],
.vc-nsfw-img [class^=wrapperPaused] {
filter: blur(${Settings.plugins.BlurNSFW.blurAmount}px);
transition: filter 0.2s;
}
.vc-nsfw-img [class^=imageWrapper]:hover img,
.vc-nsfw-img [class^=wrapperPaused]:hover video {
filter: unset;
&:hover {
filter: blur(0);
}
}
`;
}
@ -54,7 +54,7 @@ export default definePlugin({
options: {
blurAmount: {
type: OptionType.NUMBER,
description: "Blur Amount",
description: "Blur Amount (in pixels)",
default: 10,
onChange: setCss
}

View file

@ -1,29 +1,29 @@
.client-theme-settings {
.vc-clientTheme-settings {
display: flex;
flex-direction: column;
}
.client-theme-container {
.vc-clientTheme-container {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.client-theme-settings-labels {
.vc-clientTheme-labels {
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.client-theme-container > [class^="colorSwatch"] > [class^="swatch"] {
.vc-clientTheme-container [class^="swatch"] {
border: thin solid var(--background-modifier-accent) !important;
}
.client-theme-warning * {
.vc-clientTheme-warning-text {
color: var(--text-danger);
}
.client-theme-contrast-warning {
.vc-clientTheme-contrast-warning {
background-color: var(--background-primary);
padding: 0.5rem;
border-radius: .5rem;

View file

@ -7,6 +7,7 @@
import "./clientTheme.css";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
@ -14,6 +15,8 @@ import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
const cl = classNameFactory("vc-clientTheme-");
const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
const colorPresets = [
@ -60,9 +63,9 @@ function ThemeSettings() {
}
return (
<div className="client-theme-settings">
<div className="client-theme-container">
<div className="client-theme-settings-labels">
<div className={cl("settings")}>
<div className={cl("container")}>
<div className={cl("settings-labels")}>
<Forms.FormTitle tag="h3">Theme Color</Forms.FormTitle>
<Forms.FormText>Add a color to your Discord client theme</Forms.FormText>
</div>
@ -76,10 +79,10 @@ function ThemeSettings() {
{(contrastWarning || nitroThemeEnabled) && (<>
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
<div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}>
<div className="client-theme-warning">
<Forms.FormText>Warning, your theme won't look good:</Forms.FormText>
{contrastWarning && <Forms.FormText>Selected color won't contrast well with text</Forms.FormText>}
{nitroThemeEnabled && <Forms.FormText>Nitro themes aren't supported</Forms.FormText>}
<div className={cl("warning")}>
<Forms.FormText className={cl("warning-text")}>Warning, your theme won't look good:</Forms.FormText>
{contrastWarning && <Forms.FormText className={cl("warning-text")}>Selected color won't contrast well with text</Forms.FormText>}
{nitroThemeEnabled && <Forms.FormText className={cl("warning-text")}>Nitro themes aren't supported</Forms.FormText>}
</div>
{(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>}
{(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>}
@ -91,15 +94,12 @@ function ThemeSettings() {
const settings = definePluginSettings({
color: {
description: "Color your Discord client theme will be based around. Light mode isn't supported",
type: OptionType.COMPONENT,
default: "313338",
component: () => <ThemeSettings />
component: ThemeSettings
},
resetColor: {
description: "Reset Theme Color",
type: OptionType.COMPONENT,
default: "313338",
component: () => (
<Button onClick={() => onPickColor(0x313338)}>
Reset Theme Color
@ -126,18 +126,20 @@ export default definePlugin({
stop() {
document.getElementById("clientThemeVars")?.remove();
document.getElementById("clientThemeOffsets")?.remove();
document.getElementById("clientThemeLightModeFixes")?.remove();
}
});
const variableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g;
const visualRefreshVariableRegex = /(--neutral-\d{1,3}-hsl):.*?(\S*)%;/g;
const oldVariableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g;
const lightVariableRegex = /^--primary-[1-5]\d{2}-hsl/g;
const darkVariableRegex = /^--primary-[5-9]\d{2}-hsl/g;
// generates variables per theme by:
// - matching regex (so we can limit what variables are included in light/dark theme, otherwise text becomes unreadable)
// - offset from specified center (light/dark theme get different offsets because light uses 100 for background-primary, while dark uses 600)
function genThemeSpecificOffsets(variableLightness: Record<string, number>, regex: RegExp, centerVariable: string): string {
return Object.entries(variableLightness).filter(([key]) => key.search(regex) > -1)
function genThemeSpecificOffsets(variableLightness: Record<string, number>, regex: RegExp | null, centerVariable: string): string {
return Object.entries(variableLightness).filter(([key]) => regex == null || key.search(regex) > -1)
.map(([key, lightness]) => {
const lightnessOffset = lightness - variableLightness[centerVariable];
const plusOrMinus = lightnessOffset >= 0 ? "+" : "-";
@ -146,25 +148,28 @@ function genThemeSpecificOffsets(variableLightness: Record<string, number>, rege
.join("\n");
}
function generateColorOffsets(styles) {
const variableLightness = {} as Record<string, number>;
const oldVariableLightness = {} as Record<string, number>;
const visualRefreshVariableLightness = {} as Record<string, number>;
// Get lightness values of --primary variables
let variableMatch = variableRegex.exec(styles);
while (variableMatch !== null) {
const [, variable, lightness] = variableMatch;
variableLightness[variable] = parseFloat(lightness);
variableMatch = variableRegex.exec(styles);
for (const [, variable, lightness] of styles.matchAll(oldVariableRegex)) {
oldVariableLightness[variable] = parseFloat(lightness);
}
for (const [, variable, lightness] of styles.matchAll(visualRefreshVariableRegex)) {
visualRefreshVariableLightness[variable] = parseFloat(lightness);
}
createStyleSheet("clientThemeOffsets", [
`.theme-light {\n ${genThemeSpecificOffsets(variableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`,
`.theme-dark {\n ${genThemeSpecificOffsets(variableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`,
`.theme-light {\n ${genThemeSpecificOffsets(oldVariableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`,
`.theme-dark {\n ${genThemeSpecificOffsets(oldVariableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`,
`.visual-refresh.theme-light {\n ${genThemeSpecificOffsets(visualRefreshVariableLightness, null, "--neutral-2-hsl")} \n}`,
`.visual-refresh.theme-dark {\n ${genThemeSpecificOffsets(visualRefreshVariableLightness, null, "--neutral-69-hsl")} \n}`,
].join("\n\n"));
}
function generateLightModeFixes(styles) {
function generateLightModeFixes(styles: string) {
const groupLightUsesW500Regex = /\.theme-light[^{]*\{[^}]*var\(--white-500\)[^}]*}/gm;
// get light capturing groups that mention --white-500
const relevantStyles = [...styles.matchAll(groupLightUsesW500Regex)].flat();

View file

@ -5,8 +5,11 @@
*/
import { definePluginSettings } from "@api/Settings";
import { ErrorBoundary, Flex } from "@components/index";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { Margins } from "@utils/margins";
import definePlugin, { defineDefault, OptionType, StartAt } from "@utils/types";
import { Checkbox, Forms, Text } from "@webpack/common";
const Noop = () => { };
const NoopLogger = {
@ -24,6 +27,48 @@ const NoopLogger = {
const logAllow = new Set();
interface AllowLevels {
error: boolean;
warn: boolean;
trace: boolean;
log: boolean;
info: boolean;
debug: boolean;
}
interface AllowLevelSettingProps {
settingKey: keyof AllowLevels;
}
function AllowLevelSetting({ settingKey }: AllowLevelSettingProps) {
const { allowLevel } = settings.use(["allowLevel"]);
const value = allowLevel[settingKey];
return (
<Checkbox
value={value}
onChange={(_, newValue) => settings.store.allowLevel[settingKey] = newValue}
size={20}
>
<Text variant="text-sm/normal">{settingKey[0].toUpperCase() + settingKey.slice(1)}</Text>
</Checkbox>
);
}
const AllowLevelSettings = ErrorBoundary.wrap(() => {
return (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Filter List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Always allow loggers of these types</Forms.FormText>
<Flex flexDirection="row">
{Object.keys(settings.store.allowLevel).map(key => (
<AllowLevelSetting key={key} settingKey={key as keyof AllowLevels} />
))}
</Flex>
</Forms.FormSection>
);
});
const settings = definePluginSettings({
disableLoggers: {
type: OptionType.BOOLEAN,
@ -45,6 +90,18 @@ const settings = definePluginSettings({
logAllow.clear();
newVal.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));
}
},
allowLevel: {
type: OptionType.COMPONENT,
component: AllowLevelSettings,
default: defineDefault<AllowLevels>({
error: true,
warn: false,
trace: false,
log: false,
info: false,
debug: false
})
}
});
@ -60,17 +117,19 @@ export default definePlugin({
this.settings.store.whitelistedLoggers?.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));
},
Noop,
NoopLogger: () => NoopLogger,
shouldLog(logger: string) {
return logAllow.has(logger);
shouldLog(logger: string, level: keyof AllowLevels) {
return logAllow.has(logger) || settings.store.allowLevel[level] === true;
},
patches: [
{
find: "https://github.com/highlightjs/highlight.js/issues/2277",
replacement: {
match: /(?<=&&\()console.log\(`Deprecated.+?`\),/,
replace: ""
match: /\(console.log\(`Deprecated.+?`\),/,
replace: "("
}
},
{
@ -90,16 +149,15 @@ export default definePlugin({
{
find: "is not a valid locale.",
replacement: {
match: /\i\.error\(""\.concat\(\i," is not a valid locale."\)\);/,
replace: ""
match: /\i\.error(?=\(""\.concat\(\i," is not a valid locale."\)\))/,
replace: "$self.Noop"
}
},
{
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");',
all: true,
find: '"AppCrashedFatalReport: getLastCrash not supported."',
replacement: {
match: /console\.warn\("\[DEPRECATED\] Please use `subscribeWithSelector` middleware"\);/,
replace: ""
match: /console\.log(?=\("AppCrashedFatalReport: getLastCrash not supported\."\))/,
replace: "$self.Noop"
}
},
{
@ -137,13 +195,13 @@ export default definePlugin({
replace: ""
}
},
// Patches discords generic logger function
// Patches Discord generic logger function
{
find: "Σ:",
predicate: () => settings.store.disableLoggers,
replacement: {
match: /(?<=&&)(?=console)/,
replace: "$self.shouldLog(arguments[0])&&"
replace: "$self.shouldLog(arguments[0],arguments[1])&&"
}
},
{

View file

@ -63,7 +63,7 @@ function makeShortcuts() {
default:
const uniqueMatches = [...new Set(matches)];
if (uniqueMatches.length > 1)
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches);
console.warn(`Warning: This filter matches ${uniqueMatches.length} exports. Make it more specific!\n`, uniqueMatches);
return matches[0];
}
@ -82,6 +82,8 @@ function makeShortcuts() {
wp: Webpack,
wpc: { getter: () => Webpack.cache },
wreq: { getter: () => Webpack.wreq },
wpPatcher: { getter: () => Vencord.WebpackPatcher },
wpInstances: { getter: () => Vencord.WebpackPatcher.allWebpackInstances },
wpsearch: search,
wpex: extract,
wpexs: (code: string) => extract(findModuleId(code)!),
@ -151,13 +153,16 @@ function makeShortcuts() {
openModal: { getter: () => ModalAPI.openModal },
openModalLazy: { getter: () => ModalAPI.openModalLazy },
Stores: {
getter: () => Object.fromEntries(
Common.Flux.Store.getAll()
.map(store => [store.getName(), store] as const)
.filter(([name]) => name.length > 1)
)
}
Stores: Webpack.fluxStores,
// e.g. "2024-05_desktop_visual_refresh", 0
setExperiment: (id: string, bucket: number) => {
Common.FluxDispatcher.dispatch({
type: "EXPERIMENT_OVERRIDE_BUCKET",
experimentId: id,
experimentBucket: bucket,
});
},
};
}
@ -165,11 +170,38 @@ function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
const currentVal = val.getter();
if (!currentVal || val.preload === false) return currentVal;
const value = currentVal[SYM_LAZY_GET]
? forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED]
: currentVal;
function unwrapProxy(value: any) {
if (value[SYM_LAZY_GET]) {
forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED];
} else if (value.$$vencordInternal) {
return forceLoad ? value.$$vencordInternal() : value;
}
if (value) define(window.shortcutList, key, { value });
return value;
}
const value = unwrapProxy(currentVal);
if (typeof value === "object" && value !== null) {
const descriptors = Object.getOwnPropertyDescriptors(value);
for (const propKey in descriptors) {
if (value[propKey] == null) continue;
const descriptor = descriptors[propKey];
if (descriptor.writable === true || descriptor.set != null) {
const currentValue = value[propKey];
const newValue = unwrapProxy(currentValue);
if (newValue != null && currentValue !== newValue) {
value[propKey] = newValue;
}
}
}
}
if (value != null) {
define(window.shortcutList, key, { value });
define(window, key, { value });
}
return value;
}

View file

@ -33,11 +33,11 @@ function getEmojiMarkdown(target: Target, copyUnicode: boolean): string {
: `:${emojiName}:`;
}
const extension = target?.firstChild.src.match(
/https:\/\/cdn\.discordapp\.com\/emojis\/\d+\.(\w+)/
)?.[1];
const url = new URL(target.firstChild.src);
const hasParam = url.searchParams.get("animated") === "true";
const isGif = url.pathname.endsWith(".gif");
return `<${extension === "gif" ? "a" : ""}:${emojiName.replace(/~\d+$/, "")}:${emojiId}>`;
return `<${(hasParam || isGif) ? "a" : ""}:${emojiName.replace(/~\d+$/, "")}:${emojiId}>`;
}
const settings = definePluginSettings({
@ -55,7 +55,7 @@ export default definePlugin({
settings,
contextMenus: {
"expression-picker"(children, { target }: { target: Target }) {
"expression-picker"(children, { target }: { target: Target; }) {
if (target.dataset.type !== "emoji") return;
children.push(

View file

@ -173,6 +173,15 @@ export default definePlugin({
} catch (err) {
CrashHandlerLogger.debug("Failed to pop all layers.", err);
}
try {
FluxDispatcher.dispatch({
type: "DEV_TOOLS_SETTINGS_UPDATE",
settings: { displayTools: false, lastOpenTabId: "analytics" }
});
} catch (err) {
CrashHandlerLogger.debug("Failed to close DevTools.", err);
}
if (settings.store.attemptToNavigateToHome) {
try {
NavigationRouter.transitionToGuild("@me");
@ -181,7 +190,6 @@ export default definePlugin({
}
}
// Set isRecovering to false before setting the state to allow us to handle the next crash error correcty, in case it happens
setImmediate(() => isRecovering = false);

View file

@ -42,10 +42,11 @@ export default definePlugin({
// Only one of the two patches will be at effect; Discord often updates to switch between them.
// See: https://discord.com/channels/1015060230222131221/1032770730703716362/1261398512017477673
{
find: ".ENTER&&(!",
find: ".selectPreviousCommandOption(",
replacement: {
match: /(?<=(\i)\.which===\i\.\i.ENTER&&).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
replace: "$self.shouldSubmit($1, $2)"
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
match: /(?<=(\i)\.which(?:!==|===)\i\.\i.ENTER(\|\||&&)).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=(?:\|\||&&)\(\i\.preventDefault)/,
replace: (_, event, condition, codeblock) => `${condition === "||" ? "!" : ""}$self.shouldSubmit(${event},${codeblock})`
}
},
{

View file

@ -47,19 +47,11 @@ export default definePlugin({
{
match: /\i\.\i\.dispatch\({type:"IDLE",idle:!1}\)/,
replace: "$self.handleOnline()"
},
{
match: /(setInterval\(\i,\.25\*)\i\.\i/,
replace: "$1$self.getIntervalDelay()" // For web installs
}
]
}
],
getIntervalDelay() {
return Math.min(6e5, this.getIdleTimeout());
},
handleOnline() {
if (!settings.store.remainInIdle) {
FluxDispatcher.dispatch({

View file

@ -121,6 +121,7 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
height="24px"
viewBox="0 0 36 36"
aria-label="Toggle Dearrow"
className="vc-dearrow-icon"
>
<path
fill="#1213BD"

View file

@ -1,4 +1,4 @@
.vc-dearrow-toggle-off svg {
.vc-dearrow-toggle-off .vc-dearrow-icon {
filter: grayscale(1);
}

View file

@ -50,12 +50,24 @@ export default definePlugin({
find: ".decorationGridItem,",
replacement: [
{
match: /(?<==)\i=>{let{children.{20,100}decorationGridItem/,
replace: "$self.DecorationGridItem=$&"
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /(?<==)\i=>{let{children.{20,200}decorationGridItem/,
replace: "$self.DecorationGridItem=$&",
noWarn: true
},
{
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /(?<==)\i=>{let{user:\i,avatarDecoration/,
replace: "$self.DecorationGridDecoration=$&"
replace: "$self.DecorationGridDecoration=$&",
noWarn: true
},
{
match: /(?<==)\i=>{var{children.{20,200}decorationGridItem/,
replace: "$self.DecorationGridItem=$&",
},
{
match: /(?<==)\i=>{var{user:\i,avatarDecoration/,
replace: "$self.DecorationGridDecoration=$&",
},
// Remove NEW label from decor avatar decorations
{
@ -87,7 +99,7 @@ export default definePlugin({
},
// Current user area, at bottom of channels/dm list
{
find: "renderAvatarWithPopout(){",
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
replacement: [
// Use Decor avatar decoration hook
{

View file

@ -17,7 +17,6 @@ import DecorSection from "./ui/components/DecorSection";
export const settings = definePluginSettings({
changeDecoration: {
type: OptionType.COMPONENT,
description: "Change your avatar decoration",
component() {
if (!Vencord.Plugins.plugins.Decor.started) return <Forms.FormText>
Enable Decor and restart your client to change your avatar decoration.

View file

@ -5,7 +5,7 @@
*/
import { Flex } from "@components/Flex";
import { findByCodeLazy } from "@webpack";
import { findComponentByCodeLazy } from "@webpack";
import { Button, useEffect } from "@webpack/common";
import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore";
@ -13,7 +13,7 @@ import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDeco
import { cl } from "../";
import { openChangeDecorationModal } from "../modals/ChangeDecorationModal";
const CustomizationSection = findByCodeLazy(".customizationSectionBackground");
const CustomizationSection = findComponentByCodeLazy(".customizationSectionBackground");
export interface DecorSectionProps {
hideTitle?: boolean;

View file

@ -5,7 +5,7 @@
*/
import { CopyIcon, DeleteIcon } from "@components/Icons";
import { Alerts, Clipboard, ContextMenuApi, Menu, UserStore } from "webpack/common";
import { Alerts, Clipboard, ContextMenuApi, Menu, UserStore } from "@webpack/common";
import { Decoration } from "../../lib/api";
import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore";

View file

@ -19,7 +19,7 @@ import { AvatarDecorationModalPreview } from "../components";
const FileUpload = findComponentByCodeLazy("fileUploadInput,");
const { HelpMessage, HelpMessageTypes } = mapMangledModuleLazy('POSITIVE=3]="POSITIVE', {
const { HelpMessage, HelpMessageTypes } = mapMangledModuleLazy('POSITIVE="positive', {
HelpMessageTypes: filters.byProps("POSITIVE", "WARNING", "INFO"),
HelpMessage: filters.byCode(".iconDiv")
});

View file

@ -160,7 +160,7 @@ function initWs(isManual = false) {
return reply("Expected exactly one 'find' matches, found " + keys.length);
const mod = candidates[keys[0]];
let src = String(mod.original ?? mod).replaceAll("\n", "");
let src = String(mod).replaceAll("\n", "");
if (src.startsWith("function(")) {
src = "0," + src;
@ -173,7 +173,7 @@ function initWs(isManual = false) {
try {
const matcher = canonicalizeMatch(parseNode(match));
const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName");
const replacement = canonicalizeReplace(parseNode(replace), 'Vencord.Plugins.plugins["PlaceHolderPluginName"]');
const newSource = src.replace(matcher, replacement as string);

View file

@ -25,11 +25,14 @@ import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/moda
import definePlugin from "@utils/types";
import { findByCodeLazy, findStoreLazy } from "@webpack";
import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
import { Guild } from "discord-types/general";
import { Promisable } from "type-fest";
const StickersStore = findStoreLazy("StickersStore");
const uploadEmoji = findByCodeLazy(".GUILD_EMOJIS(", "EMOJI_UPLOAD_START");
const getGuildMaxEmojiSlots = findByCodeLazy(".additionalEmojiSlots") as (guild: Guild) => number;
interface Sticker {
t: "Sticker";
description: string;
@ -125,7 +128,7 @@ function getGuildCandidates(data: Data) {
const { isAnimated } = data as Emoji;
const emojiSlots = g.getMaxEmojiSlots();
const emojiSlots = getGuildMaxEmojiSlots(g);
const { emojis } = EmojiStore.getGuilds()[g.id];
let count = 0;

View file

@ -235,7 +235,7 @@ export default definePlugin({
}
},
{
find: ".PREMIUM_LOCKED;",
find: ".GUILD_SUBSCRIPTION_UNAVAILABLE;",
group: true,
predicate: () => settings.store.enableEmojiBypass,
replacement: [
@ -256,8 +256,11 @@ export default definePlugin({
},
{
// Disallow the emoji for premium locked if the intention doesn't allow it
match: /!\i\.\i\.canUseEmojisEverywhere\(\i\)/,
replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
match: /(!)?(\i\.\i\.canUseEmojisEverywhere\(\i\))/,
replace: (m, not) => not
? `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
: `(${m}||${IS_BYPASSEABLE_INTENTION})`
},
{
// Allow animated emojis to be used if the intention allows it

View file

@ -9,7 +9,7 @@ import { app } from "electron";
app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => {
frame?.once("dom-ready", () => {
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
const settings = RendererSettings.store.plugins?.FixSpotifyEmbeds;
if (!settings?.enabled) return;

View file

@ -9,7 +9,7 @@ import { app } from "electron";
app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => {
frame?.once("dom-ready", () => {
if (frame.url.startsWith("https://www.youtube.com/")) {
const settings = RendererSettings.store.plugins?.FixYoutubeEmbeds;
if (!settings?.enabled) return;

View file

@ -31,7 +31,7 @@ export default definePlugin({
{
name: "create friend invite",
description: "Generates a friend invite link.",
inputType: ApplicationCommandInputType.BOT,
inputType: ApplicationCommandInputType.BUILT_IN,
execute: async (args, ctx) => {
const invite = await FriendInvites.createFriendInvite();
@ -48,7 +48,7 @@ export default definePlugin({
{
name: "view friend invites",
description: "View a list of all generated friend invites.",
inputType: ApplicationCommandInputType.BOT,
inputType: ApplicationCommandInputType.BUILT_IN,
execute: async (_, ctx) => {
const invites = await FriendInvites.getAllFriendInvites();
const friendInviteList = invites.map(i =>
@ -67,7 +67,7 @@ export default definePlugin({
{
name: "revoke friend invites",
description: "Revokes all generated friend invites.",
inputType: ApplicationCommandInputType.BOT,
inputType: ApplicationCommandInputType.BUILT_IN,
execute: async (_, ctx) => {
await FriendInvites.revokeFriendInvites();

View file

@ -22,11 +22,11 @@ import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import { NoopComponent } from "@utils/react";
import definePlugin from "@utils/types";
import { filters, findByPropsLazy, waitFor } from "@webpack";
import { filters, findByCodeLazy, waitFor } from "@webpack";
import { ChannelStore, ContextMenuApi, UserStore } from "@webpack/common";
import { Message } from "discord-types/general";
const { useMessageMenu } = findByPropsLazy("useMessageMenu");
const useMessageMenu = findByCodeLazy(".MESSAGE,commandTargetId:");
interface CopyIdMenuItemProps {
id: string;

View file

@ -8,6 +8,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { UserStore, useStateFromStores } from "@webpack/common";
import { ReactNode } from "react";
const UserMentionComponent = findComponentByCodeLazy(".USER_MENTION)");
@ -34,14 +35,19 @@ export default definePlugin({
}
],
UserMentionComponent: ErrorBoundary.wrap((props: UserMentionComponentProps) => (
<UserMentionComponent
UserMentionComponent: ErrorBoundary.wrap((props: UserMentionComponentProps) => {
const user = useStateFromStores([UserStore], () => UserStore.getUser(props.id));
if (user == null) {
return props.originalComponent();
}
return <UserMentionComponent
// This seems to be constant
className="mention"
userId={props.id}
channelId={props.channelId}
/>
), {
/>;
}, {
fallback: ({ wrappedProps: { originalComponent } }) => originalComponent()
})
});

View file

@ -17,16 +17,15 @@
*/
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import style from "./style.css?managed";
import managedStyle from "./style.css?managed";
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:");
const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
@ -70,6 +69,7 @@ function GameActivityToggleButton() {
icon={makeIcon(showCurrentGame)}
role="switch"
aria-checked={!showCurrentGame}
redGlow={!showCurrentGame}
onClick={() => ShowCurrentGame.updateSetting(old => !old)}
/>
);
@ -90,11 +90,13 @@ export default definePlugin({
dependencies: ["UserSettingsAPI"],
settings,
managedStyle,
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
replacement: {
match: /this\.renderNameZone\(\).+?children:\[/,
match: /className:\i\.buttons,.{0,50}children:\[/,
replace: "$&$self.GameActivityToggleButton(),"
}
}
@ -102,11 +104,4 @@ export default definePlugin({
GameActivityToggleButton: ErrorBoundary.wrap(GameActivityToggleButton, { noop: true }),
start() {
enableStyle(style);
},
stop() {
disableStyle(style);
}
});

View file

@ -1,3 +1,3 @@
[class*="panels"] [class*="avatarWrapper"] {
[class^="panels"] [class^="avatarWrapper"] {
min-width: 88px;
}

Some files were not shown because too many files have changed in this diff Show more