diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f7c7363f6..8ef6503b3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,4 +1,4 @@
-name: Build latest
+name: Build DevBuild
on:
push:
branches:
@@ -9,6 +9,7 @@ on:
- browser/**
- scripts/build/**
- package.json
+ - pnpm-lock.yaml
env:
FORCE_COLOR: true
@@ -21,7 +22,7 @@ jobs:
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- - name: Use Node.js 18
+ - name: Use Node.js 19
uses: actions/setup-node@v3
with:
node-version: 19
@@ -35,7 +36,7 @@ jobs:
- name: Sign firefox extension
run: |
- pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=listed
+ pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=unlisted
env:
WEBEXT_USER: ${{ secrets.WEBEXT_USER }}
WEBEXT_SECRET: ${{ secrets.WEBEXT_SECRET }}
@@ -47,7 +48,7 @@ jobs:
run: |
mv dist/*.xpi dist/Vencord-for-Firefox.xpi
mv dist/extension-v3.zip dist/Vencord-for-Chrome-and-Edge.zip
- rm -rf dist/extension-v2-unpacked
+ rm -rf dist/extension-v2-unpacked dist/extension-v2.zip
- name: Get some values needed for the release
id: release_values
diff --git a/.gitignore b/.gitignore
index f24a72180..7bd751cb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,6 @@ lerna-debug.log*
*.tsbuildinfo
src/userplugins
+
+ExtensionCache/
+settings/
diff --git a/README.md b/README.md
index 7be2d4722..8c8466e27 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,11 @@ If you're a power user who wants to contribute and make plugins or just want to
## Installing on Browser
-Install the browser extension for [](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip), [](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Firefox.xpi) or [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js). Please note that they aren't automatically updated for now, so you will regularely have to reinstall it.
+[](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/)
+
+Or install the browser extension for
+- [](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip)
+- [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js) - Please note that QuickCSS, shiki and other plugins making use of external resources will not work with the UserScript.
You may also build them from source, to do that do the same steps as in the manual regular install method,
diff --git a/browser/GMPolyfill.js b/browser/GMPolyfill.js
new file mode 100644
index 000000000..3e0606d78
--- /dev/null
+++ b/browser/GMPolyfill.js
@@ -0,0 +1,107 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+function fetchOptions(url) {
+ return new Promise((resolve, reject) => {
+ const opt = {
+ method: "OPTIONS",
+ url: url,
+ };
+ opt.onload = resp => resolve(resp.responseHeaders);
+ opt.ontimeout = () => reject("fetch timeout");
+ opt.onerror = () => reject("fetch error");
+ opt.onabort = () => reject("fetch abort");
+ GM_xmlhttpRequest(opt);
+ });
+}
+
+function parseHeaders(headers) {
+ if (!headers)
+ return {};
+ const result = {};
+ const headersArr = headers.trim().split("\n");
+ for (var i = 0; i < headersArr.length; i++) {
+ var row = headersArr[i];
+ var index = row.indexOf(":")
+ , key = row.slice(0, index).trim().toLowerCase()
+ , value = row.slice(index + 1).trim();
+
+ if (result[key] === undefined) {
+ result[key] = value;
+ } else if (Array.isArray(result[key])) {
+ result[key].push(value);
+ } else {
+ result[key] = [result[key], value];
+ }
+ }
+ return result;
+}
+
+// returns true if CORS permits request
+async function checkCors(url, method) {
+ const headers = parseHeaders(await fetchOptions(url));
+
+ const origin = headers["access-control-allow-origin"];
+ if (origin !== "*" && origin !== window.location.origin) return false;
+
+ const methods = headers["access-control-allow-methods"]?.split(/,\s/g);
+ if (methods && !methods.includes(method)) return false;
+
+ return true;
+}
+
+function blobTo(to, blob) {
+ if (to === "arrayBuffer" && blob.arrayBuffer) return blob.arrayBuffer();
+ return new Promise((resolve, reject) => {
+ var fileReader = new FileReader();
+ fileReader.onload = event => resolve(event.target.result);
+ if (to === "arrayBuffer") fileReader.readAsArrayBuffer(blob);
+ else if (to === "text") fileReader.readAsText(blob, "utf-8");
+ else reject("unknown to");
+ });
+}
+
+function GM_fetch(url, opt) {
+ return new Promise((resolve, reject) => {
+ checkCors(url, opt?.method || "GET")
+ .then(can => {
+ if (can) {
+ // https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
+ const options = opt || {};
+ options.url = url;
+ options.data = options.body;
+ options.responseType = "blob";
+ options.onload = resp => {
+ var blob = resp.response;
+ resp.blob = () => Promise.resolve(blob);
+ resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
+ resp.text = () => blobTo("text", blob);
+ resp.json = async () => JSON.parse(await blobTo("text", blob));
+ resolve(resp);
+ };
+ options.ontimeout = () => reject("fetch timeout");
+ options.onerror = () => reject("fetch error");
+ options.onabort = () => reject("fetch abort");
+ GM_xmlhttpRequest(options);
+ } else {
+ reject("CORS issue");
+ }
+ });
+ });
+}
+export const fetch = GM_fetch;
diff --git a/browser/manifestv2.json b/browser/manifestv2.json
index c27e9e38b..405b2dc99 100644
--- a/browser/manifestv2.json
+++ b/browser/manifestv2.json
@@ -21,11 +21,5 @@
"web_accessible_resources": ["dist/Vencord.js"],
"background": {
"scripts": ["background.js"]
- },
- "browser_specific_settings": {
- "gecko": {
- "id": "vencord-firefox@vendicated.dev",
- "strict_min_version": "92.0"
- }
}
}
diff --git a/browser/userscript.meta.js b/browser/userscript.meta.js
index 81cf3e7b0..5b2a39be6 100644
--- a/browser/userscript.meta.js
+++ b/browser/userscript.meta.js
@@ -7,7 +7,7 @@
// @supportURL https://github.com/Vendicated/Vencord
// @license GPL-3.0
// @match *://*.discord.com/*
-// @grant none
+// @grant GM_xmlhttpRequest
// @run-at document-start
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
// @compatible firefox Firefox Tampermonkey
diff --git a/package.json b/package.json
index 839e87426..f0c31035f 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,8 @@
"@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
+ "@vap/core": "0.0.12",
+ "@vap/shiki": "0.10.3",
"console-menu": "^0.1.0",
"diff": "^5.1.0",
"discord-types": "^1.3.26",
@@ -50,6 +52,7 @@
"eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
+ "highlight.js": "10.6.0",
"moment": "^2.29.4",
"puppeteer-core": "^19.3.0",
"standalone-electron-types": "^1.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eaf90a1d4..6f76ff3fe 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13,6 +13,8 @@ specifiers:
'@types/yazl': ^2.4.2
'@typescript-eslint/eslint-plugin': ^5.44.0
'@typescript-eslint/parser': ^5.44.0
+ '@vap/core': 0.0.12
+ '@vap/shiki': 0.10.3
console-menu: ^0.1.0
diff: ^5.1.0
discord-types: ^1.3.26
@@ -24,6 +26,7 @@ specifiers:
eslint-plugin-simple-import-sort: ^8.0.0
eslint-plugin-unused-imports: ^2.0.0
fflate: ^0.7.4
+ highlight.js: 10.6.0
moment: ^2.29.4
puppeteer-core: ^19.3.0
standalone-electron-types: ^1.0.0
@@ -39,8 +42,10 @@ devDependencies:
'@types/react': 18.0.25
'@types/react-dom': 18.0.9
'@types/yazl': 2.4.2
- '@typescript-eslint/eslint-plugin': 5.44.0_fnsv2sbzcckq65bwfk7a5xwslu
- '@typescript-eslint/parser': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a
+ '@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au
+ '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
+ '@vap/core': 0.0.12
+ '@vap/shiki': 0.10.3
console-menu: 0.1.0
diff: 5.1.0
discord-types: 1.3.26
@@ -50,7 +55,8 @@ devDependencies:
eslint-plugin-header: 3.1.1_eslint@8.28.0
eslint-plugin-path-alias: 1.0.0_m6sma4g6bh67km3q6igf6uxaja_eslint@8.28.0
eslint-plugin-simple-import-sort: 8.0.0_eslint@8.28.0
- eslint-plugin-unused-imports: 2.0.0_aucl44mjeutxyzmt4nvo2cczya
+ eslint-plugin-unused-imports: 2.0.0_5am2datodjm2qi4eijrjrnoz54
+ highlight.js: 10.6.0
moment: 2.29.4
puppeteer-core: 19.3.0
standalone-electron-types: 1.0.0
@@ -83,9 +89,9 @@ packages:
dependencies:
ajv: 6.12.6
debug: 4.3.4
- espree: 9.4.1
- globals: 13.18.0
- ignore: 5.2.1
+ espree: 9.4.0
+ globals: 13.17.0
+ ignore: 5.2.0
import-fresh: 3.3.0
js-yaml: 4.1.0
minimatch: 3.1.2
@@ -161,7 +167,7 @@ packages:
resolution: {integrity: sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==}
dependencies:
'@types/prop-types': 15.7.5
- csstype: 3.1.1
+ csstype: 3.1.0
dev: true
/@types/react/18.0.25:
@@ -169,7 +175,7 @@ packages:
dependencies:
'@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2
- csstype: 3.1.1
+ csstype: 3.1.0
dev: true
/@types/scheduler/0.16.2:
@@ -194,8 +200,8 @@ packages:
'@types/node': 18.11.9
dev: true
- /@typescript-eslint/eslint-plugin/5.44.0_fnsv2sbzcckq65bwfk7a5xwslu:
- resolution: {integrity: sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw==}
+ /@typescript-eslint/eslint-plugin/5.45.0_czs5uoqkd3podpy6vgtsxfc7au:
+ resolution: {integrity: sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
'@typescript-eslint/parser': ^5.0.0
@@ -205,24 +211,24 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/parser': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a
- '@typescript-eslint/scope-manager': 5.44.0
- '@typescript-eslint/type-utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a
- '@typescript-eslint/utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a
+ '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
+ '@typescript-eslint/scope-manager': 5.45.0
+ '@typescript-eslint/type-utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
+ '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
debug: 4.3.4
eslint: 8.28.0
- ignore: 5.2.1
+ ignore: 5.2.0
natural-compare-lite: 1.4.0
regexpp: 3.2.0
- semver: 7.3.8
+ semver: 7.3.7
tsutils: 3.21.0_typescript@4.9.3
typescript: 4.9.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/parser/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a:
- resolution: {integrity: sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA==}
+ /@typescript-eslint/parser/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a:
+ resolution: {integrity: sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -231,9 +237,9 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/scope-manager': 5.44.0
- '@typescript-eslint/types': 5.44.0
- '@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3
+ '@typescript-eslint/scope-manager': 5.45.0
+ '@typescript-eslint/types': 5.45.0
+ '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3
debug: 4.3.4
eslint: 8.28.0
typescript: 4.9.3
@@ -241,16 +247,16 @@ packages:
- supports-color
dev: true
- /@typescript-eslint/scope-manager/5.44.0:
- resolution: {integrity: sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g==}
+ /@typescript-eslint/scope-manager/5.45.0:
+ resolution: {integrity: sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
- '@typescript-eslint/types': 5.44.0
- '@typescript-eslint/visitor-keys': 5.44.0
+ '@typescript-eslint/types': 5.45.0
+ '@typescript-eslint/visitor-keys': 5.45.0
dev: true
- /@typescript-eslint/type-utils/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a:
- resolution: {integrity: sha512-A1u0Yo5wZxkXPQ7/noGkRhV4J9opcymcr31XQtOzcc5nO/IHN2E2TPMECKWYpM3e6olWEM63fq/BaL1wEYnt/w==}
+ /@typescript-eslint/type-utils/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a:
+ resolution: {integrity: sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: '*'
@@ -259,8 +265,8 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3
- '@typescript-eslint/utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a
+ '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3
+ '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
debug: 4.3.4
eslint: 8.28.0
tsutils: 3.21.0_typescript@4.9.3
@@ -269,13 +275,13 @@ packages:
- supports-color
dev: true
- /@typescript-eslint/types/5.44.0:
- resolution: {integrity: sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ==}
+ /@typescript-eslint/types/5.45.0:
+ resolution: {integrity: sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
- /@typescript-eslint/typescript-estree/5.44.0_typescript@4.9.3:
- resolution: {integrity: sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==}
+ /@typescript-eslint/typescript-estree/5.45.0_typescript@4.9.3:
+ resolution: {integrity: sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
typescript: '*'
@@ -283,56 +289,70 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/types': 5.44.0
- '@typescript-eslint/visitor-keys': 5.44.0
+ '@typescript-eslint/types': 5.45.0
+ '@typescript-eslint/visitor-keys': 5.45.0
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
- semver: 7.3.8
+ semver: 7.3.7
tsutils: 3.21.0_typescript@4.9.3
typescript: 4.9.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/utils/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a:
- resolution: {integrity: sha512-fMzA8LLQ189gaBjS0MZszw5HBdZgVwxVFShCO3QN+ws3GlPkcy9YuS3U4wkT6su0w+Byjq3mS3uamy9HE4Yfjw==}
+ /@typescript-eslint/utils/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a:
+ resolution: {integrity: sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
'@types/json-schema': 7.0.11
'@types/semver': 7.3.13
- '@typescript-eslint/scope-manager': 5.44.0
- '@typescript-eslint/types': 5.44.0
- '@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3
+ '@typescript-eslint/scope-manager': 5.45.0
+ '@typescript-eslint/types': 5.45.0
+ '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3
eslint: 8.28.0
eslint-scope: 5.1.1
eslint-utils: 3.0.0_eslint@8.28.0
- semver: 7.3.8
+ semver: 7.3.7
transitivePeerDependencies:
- supports-color
- typescript
dev: true
- /@typescript-eslint/visitor-keys/5.44.0:
- resolution: {integrity: sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ==}
+ /@typescript-eslint/visitor-keys/5.45.0:
+ resolution: {integrity: sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
- '@typescript-eslint/types': 5.44.0
+ '@typescript-eslint/types': 5.45.0
eslint-visitor-keys: 3.3.0
dev: true
- /acorn-jsx/5.3.2_acorn@8.8.1:
+ /@vap/core/0.0.12:
+ resolution: {integrity: sha512-3csHpkE1zUSRTZwl7xIf2uXg1cD4IhhtUm0F6K/dWydc95R5Nj+krB4OTNATuqkewIv/ViCbwjPfkafAgvZQSg==}
+ dependencies:
+ eventemitter3: 4.0.7
+ dev: true
+
+ /@vap/shiki/0.10.3:
+ resolution: {integrity: sha512-tZPHZxDKEBlorQ2BaprytGfkbo5yKBvdxdAF144p94HCTpjO3ScJk/f319wi7GtV1NE4DV8HBQo/0XpldixWQA==}
+ dependencies:
+ jsonc-parser: 3.2.0
+ vscode-oniguruma: 1.7.0
+ vscode-textmate: 5.2.0
+ dev: true
+
+ /acorn-jsx/5.3.2_acorn@8.8.0:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
- acorn: 8.8.1
+ acorn: 8.8.0
dev: true
- /acorn/8.8.1:
- resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==}
+ /acorn/8.8.0:
+ resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
@@ -522,7 +542,7 @@ packages:
dev: true
/concat-map/0.0.1:
- resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
/console-menu/0.1.0:
@@ -553,8 +573,8 @@ packages:
which: 2.0.2
dev: true
- /csstype/3.1.1:
- resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
+ /csstype/3.1.0:
+ resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==}
dev: true
/debug/2.6.9:
@@ -897,7 +917,7 @@ packages:
eslint: 8.28.0
dev: true
- /eslint-plugin-unused-imports/2.0.0_aucl44mjeutxyzmt4nvo2cczya:
+ /eslint-plugin-unused-imports/2.0.0_5am2datodjm2qi4eijrjrnoz54:
resolution: {integrity: sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -907,7 +927,7 @@ packages:
'@typescript-eslint/eslint-plugin':
optional: true
dependencies:
- '@typescript-eslint/eslint-plugin': 5.44.0_fnsv2sbzcckq65bwfk7a5xwslu
+ '@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au
eslint: 8.28.0
eslint-rule-composer: 0.3.0
dev: true
@@ -971,21 +991,21 @@ packages:
eslint-scope: 7.1.1
eslint-utils: 3.0.0_eslint@8.28.0
eslint-visitor-keys: 3.3.0
- espree: 9.4.1
+ espree: 9.4.0
esquery: 1.4.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 6.0.1
find-up: 5.0.0
glob-parent: 6.0.2
- globals: 13.18.0
+ globals: 13.17.0
grapheme-splitter: 1.0.4
- ignore: 5.2.1
+ ignore: 5.2.0
import-fresh: 3.3.0
imurmurhash: 0.1.4
is-glob: 4.0.3
is-path-inside: 3.0.3
- js-sdsl: 4.2.0
+ js-sdsl: 4.1.5
js-yaml: 4.1.0
json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1
@@ -1001,12 +1021,12 @@ packages:
- supports-color
dev: true
- /espree/9.4.1:
- resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==}
+ /espree/9.4.0:
+ resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
- acorn: 8.8.1
- acorn-jsx: 5.3.2_acorn@8.8.1
+ acorn: 8.8.0
+ acorn-jsx: 5.3.2_acorn@8.8.0
eslint-visitor-keys: 3.3.0
dev: true
@@ -1039,6 +1059,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /eventemitter3/4.0.7:
+ resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+ dev: true
+
/extend-shallow/2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
@@ -1198,8 +1222,8 @@ packages:
path-is-absolute: 1.0.1
dev: true
- /globals/13.18.0:
- resolution: {integrity: sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==}
+ /globals/13.17.0:
+ resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==}
engines: {node: '>=8'}
dependencies:
type-fest: 0.20.2
@@ -1212,7 +1236,7 @@ packages:
array-union: 2.1.0
dir-glob: 3.0.1
fast-glob: 3.2.12
- ignore: 5.2.1
+ ignore: 5.2.0
merge2: 1.4.1
slash: 3.0.0
dev: true
@@ -1257,6 +1281,10 @@ packages:
kind-of: 4.0.0
dev: true
+ /highlight.js/10.6.0:
+ resolution: {integrity: sha512-8mlRcn5vk/r4+QcqerapwBYTe+iPL5ih6xrNylxrnBdHQiijDETfXX7VIxC3UiCRiINBJfANBAsPzAvRQj8RpQ==}
+ dev: true
+
/https-proxy-agent/5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
@@ -1271,8 +1299,8 @@ packages:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: true
- /ignore/5.2.1:
- resolution: {integrity: sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==}
+ /ignore/5.2.0:
+ resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==}
engines: {node: '>= 4'}
dev: true
@@ -1423,8 +1451,8 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
- /js-sdsl/4.2.0:
- resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==}
+ /js-sdsl/4.1.5:
+ resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==}
dev: true
/js-yaml/4.1.0:
@@ -1442,6 +1470,10 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true
+ /jsonc-parser/3.2.0:
+ resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
+ dev: true
+
/keypress/0.2.1:
resolution: {integrity: sha512-HjorDJFNhnM4SicvaUXac0X77NiskggxJdesG72+O5zBKpSqKFCrqmndKVqpu3pFqkla0St6uGk8Ju0sCurrmg==}
dev: true
@@ -1797,8 +1829,8 @@ packages:
ret: 0.1.15
dev: true
- /semver/7.3.8:
- resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
+ /semver/7.3.7:
+ resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==}
engines: {node: '>=10'}
hasBin: true
dependencies:
@@ -2053,6 +2085,14 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
+ /vscode-oniguruma/1.7.0:
+ resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
+ dev: true
+
+ /vscode-textmate/5.2.0:
+ resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==}
+ dev: true
+
/webidl-conversions/3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: true
diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs
index 7508937ae..c85d8aad9 100755
--- a/scripts/build/buildWeb.mjs
+++ b/scripts/build/buildWeb.mjs
@@ -60,13 +60,18 @@ await Promise.all(
}),
esbuild.build({
...commonOptions,
+ inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
+ define: {
+ "window": "unsafeWindow",
+ ...(commonOptions?.define)
+ },
outfile: "dist/Vencord.user.js",
banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`)
},
footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
- js: "Object.defineProperty(window,'Vencord',{get:()=>Vencord});"
+ js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
},
})
]
diff --git a/scripts/patcher/install.js b/scripts/patcher/install.js
index 036b0fa38..852f3a206 100755
--- a/scripts/patcher/install.js
+++ b/scripts/patcher/install.js
@@ -39,6 +39,7 @@ const {
getDarwinDirs,
getLinuxDirs,
ENTRYPOINT,
+ question
} = require("./common");
switch (process.platform) {
@@ -62,15 +63,14 @@ async function install(installations) {
// Attempt to give flatpak perms
if (selected.isFlatpak) {
try {
- const { branch } = selected;
const cwd = process.cwd();
- const globalCmd = `flatpak override ${branch} --filesystem=${cwd}`;
- const userCmd = `flatpak override --user ${branch} --filesystem=${cwd}`;
+ const globalCmd = `flatpak override ${selected.branch} --filesystem=${cwd}`;
+ const userCmd = `flatpak override --user ${selected.branch} --filesystem=${cwd}`;
const cmd = selected.location.startsWith("/home")
? userCmd
: globalCmd;
execSync(cmd);
- console.log("Successfully gave write perms to Discord Flatpak.");
+ console.log("Gave write perms to Discord Flatpak.");
} catch (e) {
console.log("Failed to give write perms to Discord Flatpak.");
console.log(
@@ -79,6 +79,29 @@ async function install(installations) {
);
process.exit(1);
}
+
+ const answer = await question(
+ `Would you like to allow ${selected.branch} to talk to org.freedesktop.Flatpak?\n` +
+ "This is essentially full host access but necessary to spawn git. Without it, the updater will not work\n" +
+ "Consider using the http based updater (using the gui installer) instead if you want to maintain the sandbox.\n" +
+ "[y/N]: "
+ );
+
+ if (["y", "yes", "yeah"].includes(answer.toLowerCase())) {
+ try {
+ const globalCmd = `flatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
+ const userCmd = `flatpak override --user ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
+ const cmd = selected.location.startsWith("/home")
+ ? userCmd
+ : globalCmd;
+ execSync(cmd);
+ console.log("Sucessfully gave talk permission");
+ } catch (err) {
+ console.error("Failed to give talk permission\n", err);
+ }
+ } else {
+ console.log(`Not giving full host access. If you change your mind later, you can run:\nflatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`);
+ }
}
for (const version of selected.versions) {
diff --git a/src/api/MessagePopover.ts b/src/api/MessagePopover.ts
new file mode 100644
index 000000000..85dff9cf5
--- /dev/null
+++ b/src/api/MessagePopover.ts
@@ -0,0 +1,69 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import Logger from "@utils/Logger";
+import { Channel, Message } from "discord-types/general";
+import type { MouseEventHandler } from "react";
+
+const logger = new Logger("MessagePopover");
+
+export interface ButtonItem {
+ key?: string,
+ label: string,
+ icon: React.ComponentType,
+ message: Message,
+ channel: Channel,
+ onClick?: MouseEventHandler,
+ onContextMenu?: MouseEventHandler;
+}
+
+export type getButtonItem = (message: Message) => ButtonItem | null;
+
+export const buttons = new Map();
+
+export function addButton(
+ identifier: string,
+ item: getButtonItem,
+) {
+ buttons.set(identifier, item);
+}
+
+export function removeButton(identifier: string) {
+ buttons.delete(identifier);
+}
+
+export function _buildPopoverElements(
+ msg: Message,
+ makeButton: (item: ButtonItem) => React.ComponentType
+) {
+ const items = [] as React.ComponentType[];
+
+ for (const [identifier, getItem] of buttons.entries()) {
+ try {
+ const item = getItem(msg);
+ if (item) {
+ item.key ??= identifier;
+ items.push(makeButton(item));
+ }
+ } catch (err) {
+ logger.error(`[${identifier}]`, err);
+ }
+ }
+
+ return items;
+}
diff --git a/src/api/index.ts b/src/api/index.ts
index 98fc6a4ac..b74da6e38 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -21,6 +21,7 @@ import * as $Commands from "./Commands";
import * as $DataStore from "./DataStore";
import * as $MessageAccessories from "./MessageAccessories";
import * as $MessageEventsAPI from "./MessageEvents";
+import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices";
import * as $ServerList from "./ServerList";
@@ -59,6 +60,10 @@ const DataStore = $DataStore;
* An API allowing you to add custom components as message accessories
*/
const MessageAccessories = $MessageAccessories;
+/**
+ * An API allowing you to add custom buttons in the message popover
+ */
+const MessagePopover = $MessagePopover;
/**
* An API allowing you to add badges to user profiles
*/
@@ -68,4 +73,4 @@ const Badges = $Badges;
*/
const ServerList = $ServerList;
-export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, Notices, ServerList };
+export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList };
diff --git a/src/api/settings.ts b/src/api/settings.ts
index b7c143a3c..2617903a5 100644
--- a/src/api/settings.ts
+++ b/src/api/settings.ts
@@ -141,14 +141,19 @@ export const Settings = makeProxy(settings);
* Settings hook for React components. Returns a smart settings
* object that automagically triggers a rerender if any properties
* are altered
+ * @param paths An optional list of paths to whitelist for rerenders
* @returns Settings
*/
-export function useSettings() {
+export function useSettings(paths?: string[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {});
+ const onUpdate: SubscriptionCallback = paths
+ ? (value, path) => paths.includes(path) && forceUpdate()
+ : forceUpdate;
+
React.useEffect(() => {
- subscriptions.add(forceUpdate);
- return () => void subscriptions.delete(forceUpdate);
+ subscriptions.add(onUpdate);
+ return () => void subscriptions.delete(onUpdate);
}, []);
return Settings;
diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx
index d1916673c..7cff58f77 100644
--- a/src/components/PluginSettings/PluginModal.tsx
+++ b/src/components/PluginSettings/PluginModal.tsx
@@ -196,7 +196,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
-
+
diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx
index 60dd96d0d..b673c4b7f 100644
--- a/src/components/VencordSettings/ThemesTab.tsx
+++ b/src/components/VencordSettings/ThemesTab.tsx
@@ -57,7 +57,8 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
{themeLinks.map(link => (
Paste links to .css / .theme.css files hereOne link per line
- Be careful to use the raw links or github.io links!
+ Make sure to use the raw links or github.io links!Find Themes:
-
+
BetterDiscord Themes
- Github
+ GitHub
If using the BD site, click on "Source" somewhere below the Download buttonIn the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button
If the theme has configuration that requires you to edit the file:
-
• Make a github account
+
• Make a GitHub account
• Click the fork button on the top right
• Edit the file
• Use the link to your own repository instead
diff --git a/src/components/VencordSettings/Updater.tsx b/src/components/VencordSettings/Updater.tsx
index bb344f5e8..33690697f 100644
--- a/src/components/VencordSettings/Updater.tsx
+++ b/src/components/VencordSettings/Updater.tsx
@@ -179,7 +179,7 @@ function Newer(props: CommonProps) {
}
function Updater() {
- const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
+ const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
React.useEffect(() => {
if (err)
diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx
index 746fcf03e..df25e2d85 100644
--- a/src/components/VencordSettings/VencordTab.tsx
+++ b/src/components/VencordSettings/VencordTab.tsx
@@ -27,7 +27,9 @@ import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common";
const st = (style: string) => `vcSettings${style}`;
function VencordSettings() {
- const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke(IpcEvents.GET_SETTINGS_DIR), "Loading...");
+ const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke(IpcEvents.GET_SETTINGS_DIR), {
+ fallbackValue: "Loading..."
+ });
const settings = useSettings();
const [donateImage] = React.useState(
@@ -87,8 +89,8 @@ function VencordSettings() {
settings.useQuickCss = v}
- note="Loads styles from your QuickCss file">
- Use QuickCss
+ note="Loads styles from your QuickCSS file">
+ Use QuickCSS
{!IS_WEB && (
@@ -101,8 +103,8 @@ function VencordSettings() {
settings.notifyAboutUpdates = v}
- note="Shows a Toast on StartUp">
- Get notified about new Updates
+ note="Shows a toast on startup">
+ Get notified about new updates
)}
@@ -129,7 +131,7 @@ function DonateCard({ image }: DonateCardProps) {
Support the Project
- Please consider supporting the Development of Vencord by donating!
+ Please consider supporting the development of Vencord by donating!
diff --git a/src/ipcMain/index.ts b/src/ipcMain/index.ts
index ba85eeb0b..86a233c71 100644
--- a/src/ipcMain/index.ts
+++ b/src/ipcMain/index.ts
@@ -89,8 +89,12 @@ export function initIpc(mainWindow: BrowserWindow) {
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
const win = new BrowserWindow({
title: "QuickCss Editor",
+ autoHideMenuBar: true,
+ darkTheme: true,
webPreferences: {
preload: join(__dirname, "preload.js"),
+ contextIsolation: true,
+ nodeIntegration: false
}
});
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
diff --git a/src/ipcMain/updater/git.ts b/src/ipcMain/updater/git.ts
index 07c94cb56..20cc5b1f0 100644
--- a/src/ipcMain/updater/git.ts
+++ b/src/ipcMain/updater/git.ts
@@ -28,10 +28,13 @@ const VENCORD_SRC_DIR = join(__dirname, "..");
const execFile = promisify(cpExecFile);
+const isFlatpak = Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
+
function git(...args: string[]) {
- return execFile("git", args, {
- cwd: VENCORD_SRC_DIR
- });
+ const opts = { cwd: VENCORD_SRC_DIR };
+
+ if (isFlatpak) return execFile("flatpak-spawn", ["--host", "git", ...args], opts);
+ else return execFile("git", args, opts);
}
async function getRepo() {
@@ -61,9 +64,13 @@ async function pull() {
}
async function build() {
- const res = await execFile("node", ["scripts/build/build.mjs"], {
- cwd: VENCORD_SRC_DIR
- });
+ const opts = { cwd: VENCORD_SRC_DIR };
+
+ let res;
+
+ if (isFlatpak) res = await execFile("flatpak-spawn", ["--host", "node", "scripts/build/build.mjs"], opts);
+ else res = await execFile("node", ["scripts/build/build.mjs"], opts);
+
return !res.stderr.includes("Build failed");
}
diff --git a/src/patcher.ts b/src/patcher.ts
index 12cefc018..0cf7e24ce 100644
--- a/src/patcher.ts
+++ b/src/patcher.ts
@@ -42,98 +42,122 @@ require.main!.filename = join(asarPath, discordPkg.main);
// @ts-ignore Untyped method? Dies from cringe
app.setAppPath(asarPath);
-// Repatch after host updates on Windows
-if (process.platform === "win32")
- require("./patchWin32Updater");
+if (!process.argv.includes("--vanilla")) {
+ // Repatch after host updates on Windows
+ if (process.platform === "win32")
+ require("./patchWin32Updater");
-class BrowserWindow extends electron.BrowserWindow {
- constructor(options: BrowserWindowConstructorOptions) {
- if (options?.webPreferences?.preload && options.title) {
- const original = options.webPreferences.preload;
- options.webPreferences.preload = join(__dirname, "preload.js");
- options.webPreferences.sandbox = false;
+ class BrowserWindow extends electron.BrowserWindow {
+ constructor(options: BrowserWindowConstructorOptions) {
+ if (options?.webPreferences?.preload && options.title) {
+ const original = options.webPreferences.preload;
+ options.webPreferences.preload = join(__dirname, "preload.js");
+ options.webPreferences.sandbox = false;
- process.env.DISCORD_PRELOAD = original;
+ process.env.DISCORD_PRELOAD = original;
- super(options);
- initIpc(this);
- } else super(options);
- }
-}
-Object.assign(BrowserWindow, electron.BrowserWindow);
-// esbuild may rename our BrowserWindow, which leads to it being excluded
-// from getFocusedWindow(), so this is necessary
-// https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
-Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
-
-// Replace electrons exports with our custom BrowserWindow
-const electronPath = require.resolve("electron");
-delete require.cache[electronPath]!.exports;
-require.cache[electronPath]!.exports = {
- ...electron,
- BrowserWindow
-};
-
-// Patch appSettings to force enable devtools
-onceDefined(global, "appSettings", s =>
- s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true)
-);
-
-process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
-
-electron.app.whenReady().then(() => {
- // Source Maps! Maybe there's a better way but since the renderer is executed
- // from a string I don't think any other form of sourcemaps would work
- electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
- let url = unsafeUrl.slice("vencord://".length);
- if (url.endsWith("/")) url = url.slice(0, -1);
- switch (url) {
- case "renderer.js.map":
- case "preload.js.map":
- case "patcher.js.map": // doubt
- cb(join(__dirname, url));
- break;
- default:
- cb({ statusCode: 403 });
+ super(options);
+ initIpc(this);
+ } else super(options);
}
- });
+ }
+ Object.assign(BrowserWindow, electron.BrowserWindow);
+ // esbuild may rename our BrowserWindow, which leads to it being excluded
+ // from getFocusedWindow(), so this is necessary
+ // https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
+ Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
- try {
- const settings = JSON.parse(readSettings());
- if (settings.enableReactDevtools)
- installExt("fmkadmapgofadopljbjfkapdkoienihi")
- .then(() => console.info("[Vencord] Installed React Developer Tools"))
- .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
- } catch { }
+ // Replace electrons exports with our custom BrowserWindow
+ const electronPath = require.resolve("electron");
+ delete require.cache[electronPath]!.exports;
+ require.cache[electronPath]!.exports = {
+ ...electron,
+ BrowserWindow
+ };
+ // Patch appSettings to force enable devtools
+ onceDefined(global, "appSettings", s =>
+ s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true)
+ );
- // Remove CSP
- function patchCsp(headers: Record, header: string) {
- if (header in headers) {
- let patchedHeader = headers[header][0];
- for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src"]) {
- patchedHeader = patchedHeader.replace(new RegExp(`${directive}.+?;`), `${directive} * blob: data: 'unsafe-inline';`);
+ process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
+
+ electron.app.whenReady().then(() => {
+ // Source Maps! Maybe there's a better way but since the renderer is executed
+ // from a string I don't think any other form of sourcemaps would work
+ electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
+ let url = unsafeUrl.slice("vencord://".length);
+ if (url.endsWith("/")) url = url.slice(0, -1);
+ switch (url) {
+ case "renderer.js.map":
+ case "preload.js.map":
+ case "patcher.js.map": // doubt
+ cb(join(__dirname, url));
+ break;
+ default:
+ cb({ statusCode: 403 });
}
- // TODO: Restrict this to only imported packages with fixed version.
- // Perhaps auto generate with esbuild
- patchedHeader = patchedHeader.replace(/script-src.+?(?=;)/, "$& 'unsafe-eval' https://unpkg.com https://cdnjs.cloudflare.com");
- headers[header] = [patchedHeader];
- }
- }
+ });
- electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, url }, cb) => {
- if (responseHeaders) {
- patchCsp(responseHeaders, "content-security-policy");
- patchCsp(responseHeaders, "content-security-policy-report-only");
+ try {
+ const settings = JSON.parse(readSettings());
+ if (settings.enableReactDevtools)
+ installExt("fmkadmapgofadopljbjfkapdkoienihi")
+ .then(() => console.info("[Vencord] Installed React Developer Tools"))
+ .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
+ } catch { }
- // Fix hosts that don't properly set the content type, such as
- // raw.githubusercontent.com
- if (url.endsWith(".css"))
- responseHeaders["content-type"] = ["text/css"];
+
+ // Remove CSP
+ type PolicyResult = Record;
+
+ const parsePolicy = (policy: string): PolicyResult => {
+ const result: PolicyResult = {};
+ policy.split(";").forEach(directive => {
+ const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
+ if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
+ result[directiveKey] = directiveValue;
+ }
+ });
+ return result;
+ };
+ const stringifyPolicy = (policy: PolicyResult): string =>
+ Object.entries(policy)
+ .filter(([, values]) => values?.length)
+ .map(directive => directive.flat().join(" "))
+ .join("; ");
+
+ function patchCsp(headers: Record, header: string) {
+ if (header in headers) {
+ const csp = parsePolicy(headers[header][0]);
+
+ for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
+ csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
+ }
+ // TODO: Restrict this to only imported packages with fixed version.
+ // Perhaps auto generate with esbuild
+ csp["script-src"] ??= [];
+ csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
+ headers[header] = [stringifyPolicy(csp)];
+ }
}
- cb({ cancel: false, responseHeaders });
+
+ electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
+ if (responseHeaders) {
+ if (resourceType === "mainFrame")
+ patchCsp(responseHeaders, "content-security-policy");
+
+ // Fix hosts that don't properly set the css content type, such as
+ // raw.githubusercontent.com
+ if (resourceType === "stylesheet")
+ responseHeaders["content-type"] = ["text/css"];
+ }
+ cb({ cancel: false, responseHeaders });
+ });
});
-});
+} else {
+ console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
+}
console.log("[Vencord] Loading original Discord app.asar");
// Legacy Vencord Injector requires "../app.asar". However, because we
diff --git a/src/plugins/apiMessagePopover.ts b/src/plugins/apiMessagePopover.ts
new file mode 100644
index 000000000..95814e05f
--- /dev/null
+++ b/src/plugins/apiMessagePopover.ts
@@ -0,0 +1,33 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+
+export default definePlugin({
+ name: "MessagePopoverAPI",
+ description: "API to add buttons to message popovers.",
+ authors: [Devs.KingFish],
+ patches: [{
+ find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
+ replacement: {
+ match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,90}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/,
+ replace: "$1...Vencord.Api.MessagePopover._buildPopoverElements($2,$4),$3"
+ }
+ }],
+});
diff --git a/src/plugins/arRPC.tsx b/src/plugins/arRPC.tsx
index cba3504af..ca94a0ecd 100644
--- a/src/plugins/arRPC.tsx
+++ b/src/plugins/arRPC.tsx
@@ -30,7 +30,7 @@ const assetManager = mapMangledModuleLazy(
}
);
-const rpcManager = findByCodeLazy(".APPLICATION_RPC(");
+const lookupRpcApp = findByCodeLazy(".APPLICATION_RPC(");
async function lookupAsset(applicationId: string, key: string): Promise {
return (await assetManager.getAsset(applicationId, [key, undefined]))[0];
@@ -39,7 +39,7 @@ async function lookupAsset(applicationId: string, key: string): Promise
const apps: any = {};
async function lookupApp(applicationId: string): Promise {
const socket: any = {};
- await rpcManager.lookupApp(socket, applicationId);
+ await lookupRpcApp(socket, applicationId);
return socket.application;
}
diff --git a/src/plugins/fakeNitro.ts b/src/plugins/fakeNitro.ts
index 0a1985a45..e5ac3b9ce 100644
--- a/src/plugins/fakeNitro.ts
+++ b/src/plugins/fakeNitro.ts
@@ -71,8 +71,8 @@ export default definePlugin({
"canUseEmojisEverywhere"
].map(func => {
return {
- match: new RegExp(`${func}:function\\(.+?}`),
- replace: `${func}:function(e){return true;}`
+ match: new RegExp(`${func}:function\\(.+?\\{`),
+ replace: "$&return true;"
};
})
},
@@ -80,8 +80,8 @@ export default definePlugin({
find: "canUseAnimatedEmojis:function",
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
replacement: {
- match: /canUseStickersEverywhere:function\(.+?}/,
- replace: "canUseStickersEverywhere:function(e){return true;}"
+ match: /canUseStickersEverywhere:function\(.+?\{/,
+ replace: "$&return true;"
},
},
{
@@ -101,8 +101,8 @@ export default definePlugin({
"canStreamMidQuality"
].map(func => {
return {
- match: new RegExp(`${func}:function\\(.+?}`),
- replace: `${func}:function(e){return true;}`
+ match: new RegExp(`${func}:function\\(.+?\\{`),
+ replace: "$&return true;"
};
})
},
diff --git a/src/plugins/HideAttachments.tsx b/src/plugins/hideAttachments.tsx
similarity index 83%
rename from src/plugins/HideAttachments.tsx
rename to src/plugins/hideAttachments.tsx
index 2c1a0d4de..944da6539 100644
--- a/src/plugins/HideAttachments.tsx
+++ b/src/plugins/hideAttachments.tsx
@@ -17,11 +17,10 @@
*/
import { get, set } from "@api/DataStore";
+import { addButton, removeButton } from "@api/MessagePopover";
import { Devs } from "@utils/constants";
-import Logger from "@utils/Logger";
import definePlugin from "@utils/types";
import { ChannelStore, FluxDispatcher } from "@webpack/common";
-import { Message } from "discord-types/general";
let style: HTMLStyleElement;
@@ -49,13 +48,7 @@ export default definePlugin({
name: "HideAttachments",
description: "Hide attachments and Embeds for individual messages via hover button",
authors: [Devs.Ven],
- patches: [{
- find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
- replacement: {
- match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,40}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/,
- replace: "$1Vencord.Plugins.plugins.HideAttachments.renderButton($2, $4),$3"
- }
- }],
+ dependencies: ["MessagePopoverAPI"],
async start() {
style = document.createElement("style");
@@ -64,11 +57,26 @@ export default definePlugin({
await getHiddenMessages();
await this.buildCss();
+
+ addButton("HideAttachments", msg => {
+ if (!msg.attachments.length && !msg.embeds.length) return null;
+
+ const isHidden = hiddenMessages.has(msg.id);
+
+ return {
+ label: isHidden ? "Show Attachments" : "Hide Attachments",
+ icon: isHidden ? ImageVisible : ImageInvisible,
+ message: msg,
+ channel: ChannelStore.getChannel(msg.channel_id),
+ onClick: () => this.toggleHide(msg.id)
+ };
+ });
},
stop() {
style.remove();
hiddenMessages.clear();
+ removeButton("HideAttachments");
},
async buildCss() {
@@ -86,26 +94,6 @@ export default definePlugin({
`;
},
- renderButton(msg: Message, makeItem: (data: any) => React.ComponentType) {
- try {
- if (!msg.attachments.length && !msg.embeds.length) return null;
-
- const isHidden = hiddenMessages.has(msg.id);
-
- return makeItem({
- key: "HideAttachments",
- label: isHidden ? "Show Attachments" : "Hide Attachments",
- icon: isHidden ? ImageVisible : ImageInvisible,
- message: msg,
- channel: ChannelStore.getChannel(msg.channel_id),
- onClick: () => this.toggleHide(msg.id)
- });
- } catch (err) {
- new Logger("HideAttachments").error(err);
- return null;
- }
- },
-
async toggleHide(id: string) {
const ids = await getHiddenMessages();
if (!ids.delete(id))
diff --git a/src/plugins/ignoreActivities.ts b/src/plugins/ignoreActivities.ts
deleted file mode 100644
index a39b02677..000000000
--- a/src/plugins/ignoreActivities.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
-*/
-
-import * as DataStore from "@api/DataStore";
-import { Devs } from "@utils/constants";
-import definePlugin from "@utils/types";
-import { findByPropsLazy } from "@webpack";
-
-interface MatchAndReplace {
- match: RegExp;
- replace: string;
-}
-
-/** Used to re-render the Registered Games tab to update how our button looks like */
-const RunningGameStoreModule = findByPropsLazy("IgnoreActivities_reRenderGames");
-
-let ignoredActivitiesCache: string[] = [];
-
-export default definePlugin({
- name: "IgnoreActivities",
- authors: [Devs.Nuckyz],
- description: "Ignore certain activities (like games) from showing up on your status. You can configure which ones are ignored from the Registered Games tab.",
- patches: [{
- find: ".Messages.SETTINGS_GAMES_OVERLAY_ON",
- replacement: [{
- match: /;(.\.renderOverlayToggle=function\(\).+?\)};)/,
- replace: (_, mod) => {
- /** Modify the renderOverlayToggle button to remove unneded stuff and render the component the way we want */
- const renderIgnoreActivitiesToggle = ([
- /** Remove overlay warn related stuff */
- { match: /,.{1,2}=.{1,2}\.overlayWarn/, replace: "" },
- { match: /,.{1,2}=.{1,2}\?\(0,.{1,2}\.jsx\)\(.{1,20}Messages\.SETTINGS_GAMES_OVERLAY_WARNING.{1,100}null/, replace: "" },
- /** Remove overlay status related stuff */
- { match: /,.{1,2}=.{1,2}\?.{1,50}Messages\.SETTINGS_GAMES_OVERLAY_OFF/, replace: "" },
- { match: /[^[]{1,2},\(0,.{1,2}\.jsx\)\("div".{1,20}\(\)\.overlayStatusText.+}\),/, replace: "" },
- /** Change the method name to renderIgnoreActivitiesToggle */
- { match: /renderOverlayToggle/, replace: "renderIgnoreActivitiesToggle" },
- /** Create an easily accessable variable to use the game props and then replace the boolean to determine if the button is activated or not with our custom function */
- { match: /((.)=this\.props\.game)(.{1,70})=.{1,2}overlay/, replace: "$1,IgnoreActivities_gameProps=$2$3=Vencord.Plugins.plugins.IgnoreActivities.isActivityEnabled(IgnoreActivities_gameProps)" },
- /** Change the handler for clicking the button */
- { match: /.\.handleOverlayToggle/, replace: "() => Vencord.Plugins.plugins.IgnoreActivities.handleActivityToggle(IgnoreActivities_gameProps)" },
- /** Change the button on component to our custom */
- { match: /(\(0,.{1,2}\.jsx\)\()(.{2})\..(.{1,50}\.overlayToggleIconOn)/, replace: "$1$2.IgnoreActivities_toggleOn$3" },
- /** Change the button off component to our custom */
- { match: /(\(0,.{1,2}\.jsx\)\()(.{2})\..{1}(.{1,50}\.overlayToggleIconOff)/, replace: "$1$2.IgnoreActivities_toggleOff$3" },
- /** Change the tooltip text */
- { match: /text:.{2}\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY/, replace: 'text:"Toggle activity"' },
- /** Change the aria-label text */
- { match: /"aria-label":.{2}\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY/, replace: '"aria-label":"Toggle activity"' }
- ] as MatchAndReplace[])
- .reduce((current, { match, replace }) => current.replace(match, replace), mod);
-
- /** Return the default renderOverlayToggle and our custom one */
- return `;${mod}${renderIgnoreActivitiesToggle}`;
-
- }
- }, {
- /** Render our ignore activity component */
- match: /(this.renderLastPlayed\(\)]}\),this.renderOverlayToggle\(\))/,
- replace: "$1,this.renderIgnoreActivitiesToggle()"
- }]
- }, {
- /** Patch the RunningGameStore to export the method to re-render the Registered Games tab */
- find: '.displayName="RunningGameStore"',
- replacement: {
- match: /(.:\(\)=>.{2})(.+function (.{2})\(\){.+\.dispatch\({type:"RUNNING_GAMES_CHANGE")/,
- replace: "$1,IgnoreActivities_reRenderGames:()=>$3$2"
- }
- }, {
- find: "M8.67872 19H11V21H7V23H17V21H13V19H20C21.103 19 22 18.104 22 17V6C22 5.89841 21.9924 5.79857 21.9777 5.70101L20 7.67872V15H12.6787L8.67872 19ZM13.1496 6H4V15H4.14961L2.00515 17.1445C2.00174 17.0967 2 17.0486 2 17V6C2 4.897 2.897 4 4 4H15.1496L13.1496 6Z",
- replacement: {
- match: /(.:\(\)=>.)(.+)(function (.)\(.{1,10}\.width.+\)\)})/s,
- replace: (_, exports, restOfFunction, component) => {
- /** Modify the overlayToggleOff component to how we want */
- const renderIgnoreActivitiesToggleOff = ([
- /** Change the method name to IgnoreActivities_toggleOffToExport */
- { match: /function ./, replace: "function IgnoreActivities_toggleOffToExport" },
- /** Change the svg path to our custom one */
- { match: /M8.67872 19H11V21H7V23H17V21H13V19H20C21.103 19 22 18.104 22 17V6C22 5.89841 21.9924 5.79857 21.9777 5.70101L20 7.67872V15H12.6787L8.67872 19ZM13.1496 6H4V15H4.14961L2.00515 17.1445C2.00174 17.0967 2 17.0486 2 17V6C2 4.897 2.897 4 4 4H15.1496L13.1496 6Z/, replace: "M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z" },
- /** Modify the view box to not cut our svg */
- { match: /viewBox:"0 0 24 24"/, replace: 'viewBox:"0 0 32 26"' },
- /** Change the rectangle coordinates to match the middle of our svg */
- { match: /x:"2"/, replace: 'x:"3"' },
- { match: /y:"20"/, replace: 'y:"26"' },
- ] as MatchAndReplace[])
- .reduce((current, { match, replace }) => current.replace(match, replace), component);
-
- /** Export our custom svg */
- return `${exports},IgnoreActivities_toggleOff:()=>IgnoreActivities_toggleOffToExport${restOfFunction}${component}${renderIgnoreActivitiesToggleOff}`;
- }
- }
- }, {
- find: "M4 2.5C2.897 2.5 2 3.397 2 4.5V15.5C2 16.604 2.897 17.5 4 17.5H11V19.5H7V21.5H17V19.5H13V17.5H20C21.103 17.5 22 16.604 22 15.5V4.5C22 3.397 21.103 2.5 20 2.5H4ZM20 4.5V13.5H4V4.5H20Z",
- replacement: {
- match: /(.:\(\)=>.)(.+)(function (.)\(.{1,10}\.width.+\)\)})/,
- replace: (_, exports, restOfFunction, component) => {
- /** Modify the overlayToggleOn svg to how we want */
- const renderIgnoreActivitiesToggleOn = ([
- /** Change the method name to IgnoreActivities_toggleOnToExport */
- { match: /function ./, replace: "function IgnoreActivities_toggleOnToExport" },
- /** Change the svg path to our custom one */
- { match: /M4 2.5C2.897 2.5 2 3.397 2 4.5V15.5C2 16.604 2.897 17.5 4 17.5H11V19.5H7V21.5H17V19.5H13V17.5H20C21.103 17.5 22 16.604 22 15.5V4.5C22 3.397 21.103 2.5 20 2.5H4ZM20 4.5V13.5H4V4.5H20Z/, replace: "M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z" },
- /** Modify the view box to not cut our svg */
- { match: /viewBox:"0 0 24 24"/, replace: 'viewBox:"0 0 32 26"' },
- ] as MatchAndReplace[])
- .reduce((current, { match, replace }) => current.replace(match, replace), component);
-
- /** Export our custom svg */
- return `${exports},IgnoreActivities_toggleOn:()=>IgnoreActivities_toggleOnToExport${restOfFunction}${component}${renderIgnoreActivitiesToggleOn}`;
- }
- }
- }, {
- /** Patch the LocalActivityStore to filter our ignored activities before they get pushed into the array */
- find: '.displayName="LocalActivityStore"',
- replacement: {
- match: /((.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?;)/,
- replace: "$1$2=$2.filter(Vencord.Plugins.plugins.IgnoreActivities.isActivityEnabled);"
- }
- }],
-
- async start() {
- ignoredActivitiesCache = (await DataStore.get("IgnoreActivities_ignoredActivities")) ?? [];
-
- if (ignoredActivitiesCache.length !== 0) {
- const gamesSeen: Record[] = RunningGameStoreModule.Z.getGamesSeen();
-
- for (const [index, ignoredActivity] of ignoredActivitiesCache.entries()) {
- if (!gamesSeen.some(game => (game.id !== undefined && game.id === ignoredActivity) || game.exePath === ignoredActivity)) {
- ignoredActivitiesCache.splice(index, 1);
- }
- }
-
- await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache);
- }
- },
-
- isActivityEnabled(props: Record) {
- /** LocalActivityStore games have a "type" prop */
- if ("type" in props) {
- if (props.application_id !== undefined) return !ignoredActivitiesCache.includes(props.application_id);
- else {
- const exePath = RunningGameStoreModule.Z.getRunningGames().find(game => game.name === props.name)?.exePath;
- if (exePath) return !ignoredActivitiesCache.includes(exePath);
- }
- }
- /** Registered Games tab games have an "exePath" prop */
- else if ("exePath" in props) {
- if (props.id !== undefined) return !ignoredActivitiesCache.includes(props.id);
- else return !ignoredActivitiesCache.includes(props.exePath);
- }
- return true;
- },
-
- async handleActivityToggle(props: Record) {
- const id = props.id ?? props.exePath;
- if (id === undefined) return;
-
- if (ignoredActivitiesCache.includes(id)) ignoredActivitiesCache.splice(ignoredActivitiesCache.indexOf(id, 1));
- else ignoredActivitiesCache.push(id);
- RunningGameStoreModule.IgnoreActivities_reRenderGames();
- await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache);
- }
-});
diff --git a/src/plugins/ignoreActivities.tsx b/src/plugins/ignoreActivities.tsx
new file mode 100644
index 000000000..981145c68
--- /dev/null
+++ b/src/plugins/ignoreActivities.tsx
@@ -0,0 +1,220 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import * as DataStore from "@api/DataStore";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Devs } from "@utils/constants";
+import { useForceUpdater } from "@utils/misc";
+import definePlugin from "@utils/types";
+import { findByPropsLazy } from "@webpack";
+import { Tooltip } from "webpack/common";
+
+enum ActivitiesTypes {
+ Game,
+ Embedded
+}
+
+interface IgnoredActivity {
+ id: string;
+ type: ActivitiesTypes;
+}
+
+const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
+const PreviewBadgeClasses = findByPropsLazy("previewBadge", "previewBadgeIcon");
+const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
+const RunningGameStore = findByPropsLazy("getRunningGames", "getGamesSeen");
+
+function ToggleIconOff() {
+ return (
+
+ );
+}
+
+function ToggleIconOn() {
+ return (
+
+ );
+}
+
+function ToggleActivityComponent({ activity }: { activity: IgnoredActivity; }) {
+ const forceUpdate = useForceUpdater();
+
+ return (
+
+ {({ onMouseLeave, onMouseEnter }) => (
+
+ );
+}
+
+function handleActivityToggle(e: React.MouseEvent, activity: IgnoredActivity, forceUpdateComponent: () => void) {
+ e.stopPropagation();
+ if (ignoredActivitiesCache.has(activity.id)) ignoredActivitiesCache.delete(activity.id);
+ else ignoredActivitiesCache.set(activity.id, activity);
+ forceUpdateComponent();
+ saveCacheToDatastore();
+}
+
+async function saveCacheToDatastore() {
+ await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache);
+}
+
+let ignoredActivitiesCache = new Map();
+
+export default definePlugin({
+ name: "IgnoreActivities",
+ authors: [Devs.Nuckyz],
+ description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.",
+ patches: [{
+ find: ".Messages.SETTINGS_GAMES_OVERLAY_ON",
+ replacement: {
+ match: /(this.renderLastPlayed\(\)]}\),this.renderOverlayToggle\(\))/,
+ replace: "$1,Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(this.props)"
+ }
+ }, {
+ find: ".Messages.NEW,name",
+ replacement: {
+ match: /\(\)\.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?.)\.name}\):null/,
+ replace: "$&,Vencord.Plugins.plugins.IgnoreActivities.renderToggleActivityButton($)"
+ }
+ }, {
+ find: '.displayName="LocalActivityStore"',
+ replacement: {
+ match: /((.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?;)/,
+ replace: "$1$2=$2.filter(Vencord.Plugins.plugins.IgnoreActivities.isActivityEnabled);"
+ }
+ }],
+
+ async start() {
+ const ignoredActivitiesData = await DataStore.get>("IgnoreActivities_ignoredActivities") ?? new Map();
+ /** Migrate old data */
+ if (Array.isArray(ignoredActivitiesData)) {
+ for (const id of ignoredActivitiesData) {
+ ignoredActivitiesCache.set(id, { id, type: ActivitiesTypes.Game });
+ }
+
+ await saveCacheToDatastore();
+ } else ignoredActivitiesCache = ignoredActivitiesData;
+
+ if (ignoredActivitiesCache.size !== 0) {
+ const gamesSeen: { id?: string; exePath: string; }[] = RunningGameStore.getGamesSeen();
+
+ for (const ignoredActivity of ignoredActivitiesCache.values()) {
+ if (ignoredActivity.type !== ActivitiesTypes.Game) continue;
+
+ if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {
+ /** Custom added game which no longer exists */
+ ignoredActivitiesCache.delete(ignoredActivity.id);
+ }
+ }
+
+ await saveCacheToDatastore();
+ }
+ },
+
+ renderToggleGameActivityButton(props: { game: { id?: string; exePath: string; } | null; }) {
+ if (!props.game) return (null);
+
+ return (
+
+
+
+ );
+ },
+
+ renderToggleActivityButton(props: { id: string; }) {
+ return (
+
+
+
+ );
+ },
+
+ isActivityEnabled(props: { type: number; application_id?: string; name?: string; }) {
+ if (props.type === 0) {
+ if (props.application_id !== undefined) return !ignoredActivitiesCache.has(props.application_id);
+ else {
+ const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
+ if (exePath) return !ignoredActivitiesCache.has(exePath);
+ }
+ }
+ return true;
+ },
+});
diff --git a/src/plugins/keepCurrentChannel.ts b/src/plugins/keepCurrentChannel.ts
index 0d7147c76..e553b9369 100644
--- a/src/plugins/keepCurrentChannel.ts
+++ b/src/plugins/keepCurrentChannel.ts
@@ -40,7 +40,7 @@ interface PreviousChannel {
export default definePlugin({
name: "KeepCurrentChannel",
- description: "Attempt to navigate the channel you were in before switching accounts or loading Discord.",
+ description: "Attempt to navigate to the channel you were in before switching accounts or loading Discord.",
authors: [Devs.Nuckyz],
isSwitchingAccount: false,
diff --git a/src/plugins/memberCount.tsx b/src/plugins/memberCount.tsx
index c016dff75..947d4d7b7 100644
--- a/src/plugins/memberCount.tsx
+++ b/src/plugins/memberCount.tsx
@@ -35,11 +35,13 @@ function MemberCount() {
if (!c) return null;
- let total = String(c[0]);
+ let total = c[0].toLocaleString();
if (total === "0" && c[1] > 0) {
total = "Loading...";
}
+ const online = c[1].toLocaleString();
+
return (
-
+
{props => (
- {c[1]}
+ {online}
)}
-
+
{props => (
.
+*/
+
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+
+export default definePlugin({
+ name: "NSFWGateBypass",
+ description: "Allows you to access NSFW channels without setting/verifying your age",
+ authors: [Devs.Commandtechno],
+ patches: [
+ {
+ find: ".nsfwAllowed=null",
+ replacement: {
+ match: /(\w+)\.nsfwAllowed=/,
+ replace: "$1.nsfwAllowed=true;",
+ },
+ },
+ ],
+});
diff --git a/src/plugins/pronoundb/components/PronounsChatComponent.tsx b/src/plugins/pronoundb/components/PronounsChatComponent.tsx
index 9225fc52b..ce67754e4 100644
--- a/src/plugins/pronoundb/components/PronounsChatComponent.tsx
+++ b/src/plugins/pronoundb/components/PronounsChatComponent.tsx
@@ -39,11 +39,10 @@ export default function PronounsChatComponentWrapper({ message }: { message: Mes
}
function PronounsChatComponent({ message }: { message: Message; }) {
- const [result, , isPending] = useAwaiter(
- () => fetchPronouns(message.author.id),
- null,
- e => console.error("Fetching pronouns failed: ", e)
- );
+ const [result, , isPending] = useAwaiter(() => fetchPronouns(message.author.id), {
+ fallbackValue: null,
+ onError: e => console.error("Fetching pronouns failed: ", e)
+ });
// If the promise completed, the result was not "unspecified", and there is a mapping for the code, then return a span with the pronouns
if (!isPending && result && result !== "unspecified" && PronounMapping[result]) {
diff --git a/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx b/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx
index 9540bb9e6..79fce23a1 100644
--- a/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx
+++ b/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx
@@ -45,11 +45,10 @@ function ProfilePronouns(
leProps: UserProfilePronounsProps;
}
) {
- const [result, , isPending] = useAwaiter(
- () => fetchPronouns(userId),
- null,
- e => console.error("Fetching pronouns failed: ", e)
- );
+ const [result, , isPending] = useAwaiter(() => fetchPronouns(userId), {
+ fallbackValue: null,
+ onError: e => console.error("Fetching pronouns failed: ", e),
+ });
// If the promise completed, the result was not "unspecified", and there is a mapping for the code, then render
if (!isPending && result && result !== "unspecified" && PronounMapping[result]) {
diff --git a/src/plugins/quickMention.tsx b/src/plugins/quickMention.tsx
index 1c0a6c6ca..6e00dd0c7 100644
--- a/src/plugins/quickMention.tsx
+++ b/src/plugins/quickMention.tsx
@@ -16,9 +16,11 @@
* along with this program. If not, see .
*/
+import { addButton, removeButton } from "@api/MessagePopover";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findLazy } from "@webpack";
+import { ChannelStore } from "@webpack/common";
const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT);
@@ -26,29 +28,22 @@ export default definePlugin({
name: "QuickMention",
authors: [Devs.kemo],
description: "Adds a quick mention button to the message actions bar",
+ dependencies: ["MessagePopoverAPI"],
- patches: [
- {
- find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
- replacement: {
- match: /(null,)(.{1,3}&&!.{1,3}\?(.{1,3})\(\{key:"reply",label:.{1,10}\.Messages\.MESSAGE_ACTION_REPLY,icon:.{1,10},channel:(.+?),message:(.+?),onClick:.+?\}\))/,
- replace: (m, post, og, functionName, channelVar, messageVar) => {
-
- const functionSig =
- `${functionName}({
- key: "QuickMention",
- label: "Mention",
- icon: Vencord.Plugins.plugins.QuickMention.Icon,
- channel: ${channelVar},
- message: ${messageVar},
- onClick: ()=> Vencord.Plugins.plugins.QuickMention.onClick(${messageVar})
- })`;
-
- return `${post}${functionSig},${og}`;
- }
- }
- }
- ],
+ start() {
+ addButton("QuickMention", msg => {
+ return {
+ label: "Quick Mention",
+ icon: this.Icon,
+ message: msg,
+ channel: ChannelStore.getChannel(msg.channel_id),
+ onClick: () => ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: `<@${msg.author.id}> ` })
+ };
+ });
+ },
+ stop() {
+ removeButton("QuickMention");
+ },
Icon: () => (
),
-
- onClick: (message: any) => ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: `<@${message.author.id}> ` })
});
diff --git a/src/plugins/reverseImageSearch.tsx b/src/plugins/reverseImageSearch.tsx
index 26e10038d..a4068ccdc 100644
--- a/src/plugins/reverseImageSearch.tsx
+++ b/src/plugins/reverseImageSearch.tsx
@@ -21,7 +21,7 @@ import definePlugin from "@utils/types";
import { Menu } from "@webpack/common";
const Engines = {
- Google: "https://www.google.com/searchbyimage?image_url=",
+ Google: "https://lens.google.com/uploadbyurl?url=",
Yandex: "https://yandex.com/images/search?rpt=imageview&url=",
SauceNAO: "https://saucenao.com/search.php?url=",
IQDB: "https://iqdb.org/?url=",
diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx
index 3815453eb..4852967a3 100644
--- a/src/plugins/reviewDB/components/ReviewsView.tsx
+++ b/src/plugins/reviewDB/components/ReviewsView.tsx
@@ -18,7 +18,7 @@
import { classes, useAwaiter } from "@utils/misc";
import { findLazy } from "@webpack";
-import { Forms, Text, UserStore } from "@webpack/common";
+import { Forms, React, Text, UserStore } from "@webpack/common";
import type { KeyboardEvent } from "react";
import { addReview, getReviews } from "../Utils/ReviewDBAPI";
@@ -27,7 +27,13 @@ import ReviewComponent from "./ReviewComponent";
const Classes = findLazy(m => typeof m.textarea === "string");
export default function ReviewsView({ userId }: { userId: string; }) {
- const [reviews, _, isLoading, refetch] = useAwaiter(() => getReviews(userId), []);
+ const [refetchCount, setRefetchCount] = React.useState(0);
+ const [reviews, _, isLoading] = useAwaiter(() => getReviews(userId), {
+ fallbackValue: [],
+ deps: [refetchCount],
+ });
+
+ const dirtyRefetch = () => setRefetchCount(refetchCount + 1);
if (isLoading) return null;
@@ -40,7 +46,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
}).then(res => {
if (res === 0 || res === 1) {
(target as HTMLInputElement).value = ""; // clear the input
- refetch();
+ dirtyRefetch();
}
});
}
@@ -62,7 +68,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
)}
{reviews?.length === 0 && (
diff --git a/src/plugins/shikiCodeblocks/api/languages.ts b/src/plugins/shikiCodeblocks/api/languages.ts
new file mode 100644
index 000000000..f14a4dc25
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/api/languages.ts
@@ -0,0 +1,74 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { ILanguageRegistration } from "@vap/shiki";
+
+export const VPC_REPO = "Vap0r1ze/vapcord";
+export const VPC_REPO_COMMIT = "88a7032a59cca40da170926651b08201ea3b965a";
+export const vpcRepoAssets = `https://raw.githubusercontent.com/${VPC_REPO}/${VPC_REPO_COMMIT}/assets/shiki-codeblocks`;
+export const vpcRepoGrammar = (fileName: string) => `${vpcRepoAssets}/${fileName}`;
+export const vpcRepoLanguages = `${vpcRepoAssets}/languages.json`;
+
+export interface Language {
+ name: string;
+ id: string;
+ devicon?: string;
+ grammarUrl: string,
+ grammar?: ILanguageRegistration["grammar"];
+ scopeName: string;
+ aliases?: string[];
+ custom?: boolean;
+}
+export interface LanguageJson {
+ name: string;
+ id: string;
+ fileName: string;
+ devicon?: string;
+ scopeName: string;
+ aliases?: string[];
+}
+
+export const languages: Record = {};
+
+export const loadLanguages = async () => {
+ const langsJson: LanguageJson[] = await fetch(vpcRepoLanguages).then(res => res.json());
+ const loadedLanguages = Object.fromEntries(
+ langsJson.map(lang => [lang.id, {
+ ...lang,
+ grammarUrl: vpcRepoGrammar(lang.fileName),
+ }])
+ );
+ Object.assign(languages, loadedLanguages);
+};
+
+export const getGrammar = (lang: Language): Promise> => {
+ if (lang.grammar) return Promise.resolve(lang.grammar);
+ return fetch(lang.grammarUrl).then(res => res.json());
+};
+
+const aliasCache = new Map();
+export function resolveLang(idOrAlias: string) {
+ if (Object.prototype.hasOwnProperty.call(languages, idOrAlias)) return languages[idOrAlias];
+
+ const lang = Object.values(languages).find(lang => lang.aliases?.includes(idOrAlias));
+
+ if (!lang) return null;
+
+ aliasCache.set(idOrAlias, lang);
+ return lang;
+}
diff --git a/src/plugins/shikiCodeblocks/api/shiki.ts b/src/plugins/shikiCodeblocks/api/shiki.ts
new file mode 100644
index 000000000..e7691ce32
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/api/shiki.ts
@@ -0,0 +1,119 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+
+import { shikiOnigasmSrc, shikiWorkerSrc } from "@utils/dependencies";
+import { WorkerClient } from "@vap/core/ipc";
+import type { IShikiTheme, IThemedToken } from "@vap/shiki";
+
+import { dispatchTheme } from "../hooks/useTheme";
+import type { ShikiSpec } from "../types";
+import { getGrammar, languages, loadLanguages, resolveLang } from "./languages";
+import { themes } from "./themes";
+
+const themeUrls = Object.values(themes);
+
+let resolveClient: (client: WorkerClient) => void;
+
+export const shiki = {
+ client: null as WorkerClient | null,
+ currentTheme: null as IShikiTheme | null,
+ currentThemeUrl: null as string | null,
+ timeoutMs: 10000,
+ languages,
+ themes,
+ loadedThemes: new Set(),
+ loadedLangs: new Set(),
+ clientPromise: new Promise>(resolve => resolveClient = resolve),
+
+ init: async (initThemeUrl: string | undefined) => {
+ /** https://stackoverflow.com/q/58098143 */
+ const workerBlob = await fetch(shikiWorkerSrc).then(res => res.blob());
+
+ const client = shiki.client = new WorkerClient(
+ "shiki-client",
+ "shiki-host",
+ workerBlob,
+ { name: "ShikiWorker" },
+ );
+ await client.init();
+
+ const themeUrl = initThemeUrl || themeUrls[0];
+
+ await loadLanguages();
+ await client.run("setOnigasm", { wasm: shikiOnigasmSrc });
+ await client.run("setHighlighter", { theme: themeUrl, langs: [] });
+ shiki.loadedThemes.add(themeUrl);
+ await shiki._setTheme(themeUrl);
+ resolveClient(client);
+ },
+ _setTheme: async (themeUrl: string) => {
+ shiki.currentThemeUrl = themeUrl;
+ const { themeData } = await shiki.client!.run("getTheme", { theme: themeUrl });
+ shiki.currentTheme = JSON.parse(themeData);
+ dispatchTheme({ id: themeUrl, theme: shiki.currentTheme });
+ },
+ loadTheme: async (themeUrl: string) => {
+ const client = await shiki.clientPromise;
+ if (shiki.loadedThemes.has(themeUrl)) return;
+
+ await client.run("loadTheme", { theme: themeUrl });
+
+ shiki.loadedThemes.add(themeUrl);
+ },
+ setTheme: async (themeUrl: string) => {
+ await shiki.clientPromise;
+ themeUrl ||= themeUrls[0];
+ if (!shiki.loadedThemes.has(themeUrl)) await shiki.loadTheme(themeUrl);
+
+ await shiki._setTheme(themeUrl);
+ },
+ loadLang: async (langId: string) => {
+ const client = await shiki.clientPromise;
+ const lang = resolveLang(langId);
+
+ if (!lang || shiki.loadedLangs.has(lang.id)) return;
+
+ await client.run("loadLanguage", {
+ lang: {
+ ...lang,
+ grammar: lang.grammar ?? await getGrammar(lang),
+ }
+ });
+ shiki.loadedLangs.add(lang.id);
+ },
+ tokenizeCode: async (code: string, langId: string): Promise => {
+ const client = await shiki.clientPromise;
+ const lang = resolveLang(langId);
+ if (!lang) return [];
+
+ if (!shiki.loadedLangs.has(lang.id)) await shiki.loadLang(lang.id);
+
+ return await client.run("codeToThemedTokens", {
+ code,
+ lang: langId,
+ theme: shiki.currentThemeUrl ?? themeUrls[0],
+ });
+ },
+ destroy() {
+ shiki.currentTheme = null;
+ shiki.currentThemeUrl = null;
+ dispatchTheme({ id: null, theme: null });
+ shiki.client?.destroy();
+ }
+};
diff --git a/src/plugins/shikiCodeblocks/api/themes.ts b/src/plugins/shikiCodeblocks/api/themes.ts
new file mode 100644
index 000000000..f31ce60b3
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/api/themes.ts
@@ -0,0 +1,67 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { IShikiTheme } from "@vap/shiki";
+
+export const SHIKI_REPO = "shikijs/shiki";
+export const SHIKI_REPO_COMMIT = "0b28ad8ccfbf2615f2d9d38ea8255416b8ac3043";
+export const shikiRepoTheme = (name: string) => `https://raw.githubusercontent.com/${SHIKI_REPO}/${SHIKI_REPO_COMMIT}/packages/shiki/themes/${name}.json`;
+
+export const themes = {
+ // Default
+ DarkPlus: shikiRepoTheme("dark-plus"),
+
+ // Dev Choices
+ MaterialCandy: "https://raw.githubusercontent.com/millsp/material-candy/master/material-candy.json",
+
+ // More from Shiki repo
+ DraculaSoft: shikiRepoTheme("dracula-soft"),
+ Dracula: shikiRepoTheme("dracula"),
+ GithubDarkDimmed: shikiRepoTheme("github-dark-dimmed"),
+ GithubDark: shikiRepoTheme("github-dark"),
+ GithubLight: shikiRepoTheme("github-light"),
+ LightPlus: shikiRepoTheme("light-plus"),
+ MaterialDarker: shikiRepoTheme("material-darker"),
+ MaterialDefault: shikiRepoTheme("material-default"),
+ MaterialLighter: shikiRepoTheme("material-lighter"),
+ MaterialOcean: shikiRepoTheme("material-ocean"),
+ MaterialPalenight: shikiRepoTheme("material-palenight"),
+ MinDark: shikiRepoTheme("min-dark"),
+ MinLight: shikiRepoTheme("min-light"),
+ Monokai: shikiRepoTheme("monokai"),
+ Nord: shikiRepoTheme("nord"),
+ OneDarkPro: shikiRepoTheme("one-dark-pro"),
+ Poimandres: shikiRepoTheme("poimandres"),
+ RosePineDawn: shikiRepoTheme("rose-pine-dawn"),
+ RosePineMoon: shikiRepoTheme("rose-pine-moon"),
+ RosePine: shikiRepoTheme("rose-pine"),
+ SlackDark: shikiRepoTheme("slack-dark"),
+ SlackOchin: shikiRepoTheme("slack-ochin"),
+ SolarizedDark: shikiRepoTheme("solarized-dark"),
+ SolarizedLight: shikiRepoTheme("solarized-light"),
+ VitesseDark: shikiRepoTheme("vitesse-dark"),
+ VitesseLight: shikiRepoTheme("vitesse-light"),
+ CssVariables: shikiRepoTheme("css-variables"),
+};
+
+export const themeCache = new Map();
+
+export const getTheme = (url: string): Promise => {
+ if (themeCache.has(url)) return Promise.resolve(themeCache.get(url)!);
+ return fetch(url).then(res => res.json());
+};
diff --git a/src/plugins/shikiCodeblocks/components/ButtonRow.tsx b/src/plugins/shikiCodeblocks/components/ButtonRow.tsx
new file mode 100644
index 000000000..e73eb72bd
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/components/ButtonRow.tsx
@@ -0,0 +1,46 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { Clipboard } from "@webpack/common";
+
+import { cl } from "../utils/misc";
+import { CopyButton } from "./CopyButton";
+
+export interface ButtonRowProps {
+ theme: import("./Highlighter").ThemeBase;
+ content: string;
+}
+
+export function ButtonRow({ content, theme }: ButtonRowProps) {
+ const buttons: JSX.Element[] = [];
+
+ if (Clipboard.SUPPORTS_COPY) {
+ buttons.push(
+
+ );
+ }
+
+ return
{buttons}
;
+}
diff --git a/src/plugins/shikiCodeblocks/components/Code.tsx b/src/plugins/shikiCodeblocks/components/Code.tsx
new file mode 100644
index 000000000..ce6a70584
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/components/Code.tsx
@@ -0,0 +1,93 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import type { IThemedToken } from "@vap/shiki";
+import { hljs } from "@webpack/common";
+
+import { cl } from "../utils/misc";
+import { ThemeBase } from "./Highlighter";
+
+export interface CodeProps {
+ theme: ThemeBase;
+ useHljs: boolean;
+ lang?: string;
+ content: string;
+ tokens: IThemedToken[][] | null;
+}
+
+export const Code = ({
+ theme,
+ useHljs,
+ lang,
+ content,
+ tokens,
+}: CodeProps) => {
+ let lines!: JSX.Element[];
+
+ if (useHljs) {
+ try {
+ const { value: hljsHtml } = hljs.highlight(lang!, content, true);
+ lines = hljsHtml
+ .split("\n")
+ .map((line, i) => );
+ } catch {
+ lines = content.split("\n").map(line => {line});
+ }
+ } else {
+ const renderTokens =
+ tokens ??
+ content
+ .split("\n")
+ .map(line => [{ color: theme.plainColor, content: line } as IThemedToken]);
+
+ lines = renderTokens.map(line => {
+ // [Cynthia] this makes it so when you highlight the codeblock
+ // empty lines are also selected and copied when you Ctrl+C.
+ if (line.length === 0) {
+ return {"\n"};
+ }
+
+ return (
+ <>
+ {line.map(({ content, color, fontStyle }, i) => (
+
+ {content}
+
+ ))}
+ >
+ );
+ });
+ }
+
+ const codeTableRows = lines.map((line, i) => (
+
+
{i + 1}
+
{line}
+
+ ));
+
+ return
{...codeTableRows}
;
+};
diff --git a/src/plugins/shikiCodeblocks/components/CopyButton.tsx b/src/plugins/shikiCodeblocks/components/CopyButton.tsx
new file mode 100644
index 000000000..153b3cddb
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/components/CopyButton.tsx
@@ -0,0 +1,41 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { useCopyCooldown } from "../hooks/useCopyCooldown";
+
+export interface CopyButtonProps extends React.DetailedHTMLProps, HTMLButtonElement> {
+ content: string;
+}
+
+export function CopyButton({ content, ...props }: CopyButtonProps) {
+ const [copyCooldown, copy] = useCopyCooldown(1000);
+
+ return (
+
+
+ );
+}
diff --git a/src/plugins/shikiCodeblocks/components/Header.tsx b/src/plugins/shikiCodeblocks/components/Header.tsx
new file mode 100644
index 000000000..c2db38693
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/components/Header.tsx
@@ -0,0 +1,42 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { Language } from "../api/languages";
+import { DeviconSetting } from "../types";
+import { cl } from "../utils/misc";
+
+export interface HeaderProps {
+ langName?: string;
+ useDevIcon: DeviconSetting;
+ shikiLang: Language | null;
+}
+
+export function Header({ langName, useDevIcon, shikiLang }: HeaderProps) {
+ if (!langName) return <>>;
+
+ return (
+
+ );
+}
diff --git a/src/plugins/shikiCodeblocks/components/Highlighter.tsx b/src/plugins/shikiCodeblocks/components/Highlighter.tsx
new file mode 100644
index 000000000..d26cd81a4
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/components/Highlighter.tsx
@@ -0,0 +1,131 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import ErrorBoundary from "@components/ErrorBoundary";
+import { useAwaiter } from "@utils/misc";
+import { useIntersection } from "@utils/react";
+import { hljs, React } from "@webpack/common";
+
+import { resolveLang } from "../api/languages";
+import { shiki } from "../api/shiki";
+import { useShikiSettings } from "../hooks/useShikiSettings";
+import { useTheme } from "../hooks/useTheme";
+import { hex2Rgb } from "../utils/color";
+import { cl, shouldUseHljs } from "../utils/misc";
+import { ButtonRow } from "./ButtonRow";
+import { Code } from "./Code";
+import { Header } from "./Header";
+
+export interface ThemeBase {
+ plainColor: string;
+ accentBgColor: string;
+ accentFgColor: string;
+ backgroundColor: string;
+}
+
+export interface HighlighterProps {
+ lang?: string;
+ content: string;
+ isPreview: boolean;
+ tempSettings?: Record;
+}
+
+export const createHighlighter = (props: HighlighterProps) => (
+
+ );
+};
+
diff --git a/src/plugins/shikiCodeblocks/hooks/useCopyCooldown.ts b/src/plugins/shikiCodeblocks/hooks/useCopyCooldown.ts
new file mode 100644
index 000000000..414500bdc
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/hooks/useCopyCooldown.ts
@@ -0,0 +1,34 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { Clipboard, React } from "@webpack/common";
+
+export function useCopyCooldown(cooldown: number) {
+ const [copyCooldown, setCopyCooldown] = React.useState(false);
+
+ function copy(text: string) {
+ Clipboard.copy(text);
+ setCopyCooldown(true);
+
+ setTimeout(() => {
+ setCopyCooldown(false);
+ }, cooldown);
+ }
+
+ return [copyCooldown, copy] as const;
+}
diff --git a/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts
new file mode 100644
index 000000000..50b0fc978
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts
@@ -0,0 +1,47 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { useSettings } from "@api/settings";
+import { React } from "@webpack/common";
+
+import { shiki } from "../api/shiki";
+import { ShikiSettings } from "../types";
+
+export function useShikiSettings(settingKeys: (keyof ShikiSettings)[], overrides?: Record) {
+ const settings = useSettings(settingKeys.map(key => `plugins.ShikiCodeblocks.${key}`)).plugins.ShikiCodeblocks as ShikiSettings;
+ const [isLoading, setLoading] = React.useState(false);
+
+ const withOverrides = { ...settings, ...overrides };
+ const themeUrl = withOverrides.customTheme || withOverrides.theme;
+
+ if (overrides) {
+ const willChangeTheme = shiki.currentThemeUrl && themeUrl !== shiki.currentThemeUrl;
+ const noOverrides = Object.keys(overrides).length === 0;
+
+ if (isLoading && (!willChangeTheme || noOverrides)) setLoading(false);
+ if ((!isLoading && willChangeTheme)) {
+ setLoading(true);
+ shiki.setTheme(themeUrl);
+ }
+ }
+
+ return {
+ ...withOverrides,
+ isThemeLoading: themeUrl !== shiki.currentThemeUrl,
+ };
+}
diff --git a/src/plugins/shikiCodeblocks/hooks/useTheme.ts b/src/plugins/shikiCodeblocks/hooks/useTheme.ts
new file mode 100644
index 000000000..fae579611
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/hooks/useTheme.ts
@@ -0,0 +1,49 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { React } from "@webpack/common";
+
+type Shiki = typeof import("../api/shiki").shiki;
+interface ThemeState {
+ id: Shiki["currentThemeUrl"],
+ theme: Shiki["currentTheme"],
+}
+
+const currentTheme: ThemeState = {
+ id: null,
+ theme: null,
+};
+
+const themeSetters = new Set>>();
+
+export const useTheme = (): ThemeState => {
+ const [, setTheme] = React.useState(currentTheme);
+
+ React.useEffect(() => {
+ themeSetters.add(setTheme);
+ return () => void themeSetters.delete(setTheme);
+ }, []);
+
+ return currentTheme;
+};
+
+export function dispatchTheme(state: ThemeState) {
+ if (currentTheme.id === state.id) return;
+ Object.assign(currentTheme, state);
+ themeSetters.forEach(setTheme => setTheme(state));
+}
diff --git a/src/plugins/shikiCodeblocks/index.ts b/src/plugins/shikiCodeblocks/index.ts
new file mode 100644
index 000000000..fd6b04bf7
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/index.ts
@@ -0,0 +1,164 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { Devs } from "@utils/constants";
+import { parseUrl } from "@utils/misc";
+import { wordsFromPascal, wordsToTitle } from "@utils/text";
+import definePlugin, { OptionType } from "@utils/types";
+
+import previewExampleText from "~fileContent/previewExample.tsx";
+import cssText from "~fileContent/style.css";
+
+import { Settings } from "../../Vencord";
+import { shiki } from "./api/shiki";
+import { themes } from "./api/themes";
+import { createHighlighter } from "./components/Highlighter";
+import { DeviconSetting, HljsSetting, ShikiSettings, StyleSheets } from "./types";
+import { clearStyles, removeStyle, setStyle } from "./utils/createStyle";
+
+const themeNames = Object.keys(themes);
+const devIconCss = "@import url('https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css');";
+
+const getSettings = () => Settings.plugins.ShikiCodeblocks as ShikiSettings;
+
+export default definePlugin({
+ name: "ShikiCodeblocks",
+ description: "Brings vscode-style codeblocks into Discord, powered by Shiki",
+ authors: [Devs.Vap],
+ patches: [
+ {
+ find: "codeBlock:{react:function",
+ replacement: {
+ match: /codeBlock:\{react:function\((.),(.),(.)\)\{/,
+ replace: "$&return Vencord.Plugins.plugins.ShikiCodeblocks.renderHighlighter($1,$2,$3);",
+ },
+ },
+ ],
+ start: async () => {
+ setStyle(cssText, StyleSheets.Main);
+ if (getSettings().useDevIcon !== DeviconSetting.Disabled)
+ setStyle(devIconCss, StyleSheets.DevIcons);
+
+ await shiki.init(getSettings().customTheme || getSettings().theme);
+ },
+ stop: () => {
+ shiki.destroy();
+ clearStyles();
+ },
+ settingsAboutComponent: ({ tempSettings }) => createHighlighter({
+ lang: "tsx",
+ content: previewExampleText,
+ isPreview: true,
+ tempSettings,
+ }),
+ options: {
+ theme: {
+ type: OptionType.SELECT,
+ description: "Default themes",
+ options: themeNames.map(themeName => ({
+ label: wordsToTitle(wordsFromPascal(themeName)),
+ value: themes[themeName],
+ default: themes[themeName] === themes.DarkPlus,
+ })),
+ disabled: () => !!getSettings().customTheme,
+ onChange: shiki.setTheme,
+ },
+ customTheme: {
+ type: OptionType.STRING,
+ description: "A link to a custom vscode theme",
+ placeholder: themes.MaterialCandy,
+ isValid: value => {
+ if (!value) return true;
+ const url = parseUrl(value);
+ if (!url) return "Must be a valid URL";
+
+ if (!url.pathname.endsWith(".json")) return "Must be a json file";
+
+ return true;
+ },
+ onChange: value => shiki.setTheme(value || getSettings().theme),
+ },
+ tryHljs: {
+ type: OptionType.SELECT,
+ description: "Use the more lightweight default Discord highlighter and theme.",
+ options: [
+ {
+ label: "Never",
+ value: HljsSetting.Never,
+ },
+ {
+ label: "Prefer Shiki instead of Highlight.js",
+ value: HljsSetting.Secondary,
+ default: true,
+ },
+ {
+ label: "Prefer Highlight.js instead of Shiki",
+ value: HljsSetting.Primary,
+ },
+ {
+ label: "Always",
+ value: HljsSetting.Always,
+ },
+ ],
+ },
+ useDevIcon: {
+ type: OptionType.SELECT,
+ description: "How to show language icons on codeblocks",
+ options: [
+ {
+ label: "Disabled",
+ value: DeviconSetting.Disabled,
+ },
+ {
+ label: "Colorless",
+ value: DeviconSetting.Greyscale,
+ default: true,
+ },
+ {
+ label: "Colored",
+ value: DeviconSetting.Color,
+ },
+ ],
+ onChange: (newValue: DeviconSetting) => {
+ if (newValue === DeviconSetting.Disabled) removeStyle(StyleSheets.DevIcons);
+ else setStyle(devIconCss, StyleSheets.DevIcons);
+ },
+ },
+ bgOpacity: {
+ type: OptionType.SLIDER,
+ description: "Background opacity",
+ markers: [0, 20, 40, 60, 80, 100],
+ default: 100,
+ componentProps: {
+ stickToMarkers: false,
+ onValueRender: null, // Defaults to percentage
+ },
+ },
+ },
+
+ // exports
+ shiki,
+ createHighlighter,
+ renderHighlighter: ({ lang, content }: { lang: string; content: string; }) => {
+ return createHighlighter({
+ lang,
+ content,
+ isPreview: false,
+ });
+ },
+});
diff --git a/src/plugins/shikiCodeblocks/previewExample.tsx b/src/plugins/shikiCodeblocks/previewExample.tsx
new file mode 100644
index 000000000..971d01670
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/previewExample.tsx
@@ -0,0 +1,13 @@
+/* eslint-disable header/header */
+import React from "react";
+
+const handleClick = async () =>
+ console.log((await import("@webpack/common")).Clipboard.copy("\u200b"));
+
+export const Example: React.FC<{
+ real: boolean,
+ shigged?: number,
+}> = ({ real, shigged }) => <>
+
+
+>;
diff --git a/src/plugins/shikiCodeblocks/style.css b/src/plugins/shikiCodeblocks/style.css
new file mode 100644
index 000000000..b246db4c9
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/style.css
@@ -0,0 +1,103 @@
+.shiki-root {
+ border-radius: 4px;
+
+ /* fallback background */
+ background-color: var(--background-secondary);
+}
+
+.shiki-root code {
+ display: block;
+ overflow-x: auto;
+ padding: 0.5em;
+ position: relative;
+
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ text-indent: 0;
+ white-space: pre-wrap;
+ background: transparent;
+ border: none;
+}
+
+.shiki-root [class^='devicon-'],
+.shiki-root [class*=' devicon-'] {
+ margin-right: 8px;
+ user-select: none;
+}
+
+.shiki-plain code {
+ padding-top: 8px;
+}
+
+.shiki-btns {
+ font-size: 1em;
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ opacity: 0;
+}
+
+.shiki-root:hover .shiki-btns {
+ opacity: 1;
+}
+
+.shiki-btn {
+ border-radius: 4px 4px 0 0;
+ padding: 4px 8px;
+}
+
+.shiki-btn~.shiki-btn {
+ margin-left: 4px;
+}
+
+.shiki-btn:last-child {
+ border-radius: 4px 0;
+}
+
+.shiki-spinner-container {
+ align-items: center;
+ background-color: rgba(0, 0, 0, 0.6);
+ display: flex;
+ position: absolute;
+ justify-content: center;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+}
+
+.shiki-preview {
+ margin-bottom: 2em;
+}
+
+.shiki-lang {
+ padding: 0 5px;
+ margin-bottom: 6px;
+ font-weight: bold;
+ text-transform: capitalize;
+ display: flex;
+ align-items: center;
+}
+
+.shiki-table {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+.shiki-table tr {
+ height: 19px;
+ width: 100%;
+}
+
+.shiki-root td:first-child {
+ border-right: 1px solid transparent;
+ padding-left: 5px;
+ padding-right: 8px;
+ user-select: none;
+}
+
+.shiki-root td:last-child {
+ padding-left: 8px;
+ word-break: break-word;
+ width: 100%;
+}
diff --git a/src/plugins/shikiCodeblocks/types.ts b/src/plugins/shikiCodeblocks/types.ts
new file mode 100644
index 000000000..ee5aa9e64
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/types.ts
@@ -0,0 +1,78 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import type {
+ ILanguageRegistration,
+ IShikiTheme,
+ IThemedToken,
+ IThemeRegistration,
+} from "@vap/shiki";
+
+import type { Settings } from "../../Vencord";
+
+/** This must be atleast a subset of the `@vap/shiki-worker` spec */
+export type ShikiSpec = {
+ setOnigasm: ({ wasm }: { wasm: string; }) => Promise;
+ setHighlighter: ({ theme, langs }: {
+ theme: IThemeRegistration | void;
+ langs: ILanguageRegistration[];
+ }) => Promise;
+ loadTheme: ({ theme }: {
+ theme: string | IShikiTheme;
+ }) => Promise;
+ getTheme: ({ theme }: { theme: string; }) => Promise<{ themeData: string; }>;
+ loadLanguage: ({ lang }: { lang: ILanguageRegistration; }) => Promise;
+ codeToThemedTokens: ({
+ code,
+ lang,
+ theme,
+ }: {
+ code: string;
+ lang?: string;
+ theme?: string;
+ }) => Promise;
+};
+
+export enum StyleSheets {
+ Main = "MAIN",
+ DevIcons = "DEVICONS",
+}
+
+export enum HljsSetting {
+ Never = "NEVER",
+ Secondary = "SECONDARY",
+ Primary = "PRIMARY",
+ Always = "ALWAYS",
+}
+export enum DeviconSetting {
+ Disabled = "DISABLED",
+ Greyscale = "GREYSCALE",
+ Color = "COLOR"
+}
+
+type CommonSettings = {
+ [K in keyof Settings["plugins"][string]as K extends `${infer V}` ? K : never]: Settings["plugins"][string][K];
+};
+
+export interface ShikiSettings extends CommonSettings {
+ theme: string;
+ customTheme: string;
+ tryHljs: HljsSetting;
+ useDevIcon: DeviconSetting;
+ bgOpacity: number;
+}
diff --git a/src/plugins/shikiCodeblocks/utils/color.ts b/src/plugins/shikiCodeblocks/utils/color.ts
new file mode 100644
index 000000000..e74ec526d
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/utils/color.ts
@@ -0,0 +1,32 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+export function hex2Rgb(hex: string) {
+ hex = hex.slice(1);
+ if (hex.length < 6)
+ hex = hex
+ .split("")
+ .map(c => c + c)
+ .join("");
+ if (hex.length === 6) hex += "ff";
+ if (hex.length > 6) hex = hex.slice(0, 6);
+ return hex
+ .split(/(..)/)
+ .filter(Boolean)
+ .map(c => parseInt(c, 16));
+}
diff --git a/src/plugins/shikiCodeblocks/utils/createStyle.ts b/src/plugins/shikiCodeblocks/utils/createStyle.ts
new file mode 100644
index 000000000..734f7dcb3
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/utils/createStyle.ts
@@ -0,0 +1,36 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+const styles = new Map();
+
+export function setStyle(css: string, id: string) {
+ const style = document.createElement("style");
+ style.innerText = css;
+ document.head.appendChild(style);
+ styles.set(id, style);
+}
+
+export function removeStyle(id: string) {
+ styles.get(id)?.remove();
+ return styles.delete(id);
+}
+
+export const clearStyles = () => {
+ styles.forEach(style => style.remove());
+ styles.clear();
+};
diff --git a/src/plugins/shikiCodeblocks/utils/misc.ts b/src/plugins/shikiCodeblocks/utils/misc.ts
new file mode 100644
index 000000000..1342ff5d8
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/utils/misc.ts
@@ -0,0 +1,50 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { hljs } from "@webpack/common";
+
+import { resolveLang } from "../api/languages";
+import { HighlighterProps } from "../components/Highlighter";
+import { HljsSetting, ShikiSettings } from "../types";
+
+export const cl = (className: string) => `shiki-${className}`;
+
+export const shouldUseHljs = ({
+ lang,
+ tryHljs,
+}: {
+ lang: HighlighterProps["lang"],
+ tryHljs: ShikiSettings["tryHljs"],
+}) => {
+ const hljsLang = lang ? hljs?.getLanguage?.(lang) : null;
+ const shikiLang = lang ? resolveLang(lang) : null;
+ const langName = shikiLang?.name;
+
+ switch (tryHljs) {
+ case HljsSetting.Always:
+ return true;
+ case HljsSetting.Primary:
+ return !!hljsLang || lang === "";
+ case HljsSetting.Secondary:
+ return !langName && !!hljsLang;
+ case HljsSetting.Never:
+ return false;
+ }
+
+ return false;
+};
diff --git a/src/plugins/sortFriendRequests.tsx b/src/plugins/sortFriendRequests.tsx
new file mode 100644
index 000000000..b9732afb8
--- /dev/null
+++ b/src/plugins/sortFriendRequests.tsx
@@ -0,0 +1,74 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { Flex } from "@components/Flex";
+import { Devs } from "@utils/constants";
+import definePlugin, { OptionType } from "@utils/types";
+import { RelationshipStore } from "@webpack/common";
+import { User } from "discord-types/general";
+import { Settings } from "Vencord";
+
+export default definePlugin({
+ name: "SortFriendRequests",
+ authors: [Devs.Megu],
+ description: "Sorts friend requests by date of receipt",
+
+ patches: [{
+ find: ".PENDING_INCOMING||",
+ replacement: [{
+ match: /\.sortBy\(\(function\((\w)\){return \w{1,3}\.comparator}\)\)/,
+ // If the row type is 3 or 4 (pendinng incoming or outgoing), sort by date of receipt
+ // Otherwise, use the default comparator
+ replace: (_, row) => `.sortBy((function(${row}) {
+ return ${row}.type === 3 || ${row}.type === 4
+ ? -Vencord.Plugins.plugins.SortFriendRequests.getSince(${row}.user)
+ : ${row}.comparator
+ }))`
+ }, {
+ predicate: () => Settings.plugins.SortFriendRequests.showDates,
+ match: /(user:(\w{1,3}),.{10,30}),subText:(\w{1,3}),(.{10,30}userInfo}\))/,
+ // Show dates in the friend request list
+ replace: (_, pre, user, subText, post) => `${pre},
+ subText: Vencord.Plugins.plugins.SortFriendRequests.makeSubtext(${subText}, ${user}),
+ ${post}`
+ }]
+ }],
+
+ getSince(user: User) {
+ return new Date(RelationshipStore.getSince(user.id));
+ },
+
+ makeSubtext(text: string, user: User) {
+ const since = this.getSince(user);
+ return (
+
+ {text}
+ {!isNaN(since.getTime()) && Received — {since.toDateString()}}
+
+ );
+ },
+
+ options: {
+ showDates: {
+ type: OptionType.BOOLEAN,
+ description: "Show dates on friend requests",
+ default: false,
+ restartNeeded: true
+ }
+ }
+});
diff --git a/src/plugins/viewRaw.tsx b/src/plugins/viewRaw.tsx
new file mode 100644
index 000000000..c49180b8e
--- /dev/null
+++ b/src/plugins/viewRaw.tsx
@@ -0,0 +1,147 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { addButton, removeButton } from "@api/MessagePopover";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Flex } from "@components/Flex";
+import { Devs } from "@utils/constants";
+import { copyWithToast } from "@utils/misc";
+import { closeModal, ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
+import definePlugin from "@utils/types";
+import { Button, ChannelStore, Forms, Margins, Parser } from "@webpack/common";
+import { Message } from "discord-types/general";
+
+
+const CopyIcon = () => {
+ return ;
+};
+
+function sortObject(obj: T): T {
+ return Object.fromEntries(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))) as T;
+}
+
+function cleanMessage(msg: Message) {
+ const clone = sortObject(JSON.parse(JSON.stringify(msg)));
+ for (const key in clone.author) {
+ switch (key) {
+ case "id":
+ case "username":
+ case "usernameNormalized":
+ case "discriminator":
+ case "avatar":
+ case "bot":
+ case "system":
+ case "publicFlags":
+ break;
+ default:
+ // phone number, email, etc
+ delete clone.author[key];
+ }
+ }
+
+ // message logger added properties
+ const cloneAny = clone as any;
+ delete cloneAny.editHistory;
+ delete cloneAny.deleted;
+ cloneAny.attachments?.forEach(a => delete a.deleted);
+
+ return clone;
+}
+
+function CodeBlock(props: { content: string, lang: string; }) {
+ return (
+ // make text selectable
+