Seaswimmer 2024-05-31 19:50:44 -04:00
commit c045a02598
62 changed files with 1386 additions and 283 deletions

View file

@ -1,7 +1,7 @@
{ {
"root": true, "root": true,
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"ignorePatterns": ["dist", "browser"], "ignorePatterns": ["dist", "browser", "packages/vencord-types"],
"plugins": [ "plugins": [
"@typescript-eslint", "@typescript-eslint",
"simple-header", "simple-header",

View file

@ -19,8 +19,8 @@
/// <reference path="../src/modules.d.ts" /> /// <reference path="../src/modules.d.ts" />
/// <reference path="../src/globals.d.ts" /> /// <reference path="../src/globals.d.ts" />
import monacoHtmlLocal from "~fileContent/monacoWin.html"; import monacoHtmlLocal from "file://monacoWin.html?minify";
import monacoHtmlCdn from "~fileContent/../src/main/monacoWin.html"; import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify";
import * as DataStore from "../src/api/DataStore"; import * as DataStore from "../src/api/DataStore";
import { debounce } from "../src/utils"; import { debounce } from "../src/utils";
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata"; import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.8.5", "version": "1.8.6",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -18,8 +18,9 @@
}, },
"scripts": { "scripts": {
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs", "build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
"buildStandalone": "pnpm build --standalone",
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs", "buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch", "watch": "pnpm build --watch",
"generatePluginJson": "tsx scripts/generatePluginList.ts", "generatePluginJson": "tsx scripts/generatePluginList.ts",
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types", "generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
"inject": "node scripts/runInstaller.mjs", "inject": "node scripts/runInstaller.mjs",
@ -27,7 +28,7 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins", "lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix", "lint:fix": "pnpm lint --fix",
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson", "test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc", "testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit" "testTsc": "tsc --noEmit"
}, },
@ -61,6 +62,7 @@
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"highlight.js": "10.6.0", "highlight.js": "10.6.0",
"html-minifier-terser": "^7.2.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"puppeteer-core": "^19.11.1", "puppeteer-core": "^19.11.1",
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^1.0.0",

View file

@ -98,6 +98,9 @@ importers:
highlight.js: highlight.js:
specifier: 10.6.0 specifier: 10.6.0
version: 10.6.0 version: 10.6.0
html-minifier-terser:
specifier: ^7.2.0
version: 7.2.0
moment: moment:
specifier: ^2.29.4 specifier: ^2.29.4
version: 2.29.4 version: 2.29.4
@ -393,6 +396,27 @@ packages:
'@humanwhocodes/object-schema@1.2.1': '@humanwhocodes/object-schema@1.2.1':
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
'@jridgewell/gen-mapping@0.3.5':
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/set-array@1.2.1':
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'}
'@jridgewell/source-map@0.3.6':
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
'@jridgewell/sourcemap-codec@1.4.15':
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -688,6 +712,9 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
camel-case@4.1.2:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
camelcase-keys@6.2.2: camelcase-keys@6.2.2:
resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -716,6 +743,10 @@ packages:
resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
clean-css@5.3.3:
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
engines: {node: '>= 10.0'}
cliui@8.0.1: cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -740,6 +771,13 @@ packages:
colord@2.9.3: colord@2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
commander@10.0.1:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
engines: {node: '>=14'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
component-emitter@1.3.0: component-emitter@1.3.0:
resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==}
@ -871,12 +909,19 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
end-of-stream@1.4.4: end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
error-ex@1.3.2: error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
@ -1402,6 +1447,11 @@ packages:
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
engines: {node: '>=10'} engines: {node: '>=10'}
html-minifier-terser@7.2.0:
resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==}
engines: {node: ^14.13.1 || >=16.0.0}
hasBin: true
html-tags@3.3.1: html-tags@3.3.1:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1673,6 +1723,9 @@ packages:
lodash.truncate@4.4.2: lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
lru-cache@6.0.0: lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1770,6 +1823,9 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
node-fetch@2.6.7: node-fetch@2.6.7:
resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
engines: {node: 4.x || >=6.0.0} engines: {node: 4.x || >=6.0.0}
@ -1855,6 +1911,9 @@ packages:
pako@1.0.11: pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
parent-module@1.0.1: parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1863,6 +1922,9 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'} engines: {node: '>=8'}
pascal-case@3.1.2:
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
pascalcase@0.1.1: pascalcase@0.1.1:
resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -1985,6 +2047,10 @@ packages:
resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
relateurl@0.2.7:
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
engines: {node: '>= 0.10'}
require-directory@2.1.1: require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2228,6 +2294,11 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
terser@5.31.0:
resolution: {integrity: sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==}
engines: {node: '>=10'}
hasBin: true
text-table@0.2.0: text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@ -2263,6 +2334,9 @@ packages:
tslib@1.14.1: tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
tsutils@3.21.0: tsutils@3.21.0:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -2599,6 +2673,28 @@ snapshots:
'@humanwhocodes/object-schema@1.2.1': {} '@humanwhocodes/object-schema@1.2.1': {}
'@jridgewell/gen-mapping@0.3.5':
dependencies:
'@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/set-array@1.2.1': {}
'@jridgewell/source-map@0.3.6':
dependencies:
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/sourcemap-codec@1.4.15': {}
'@jridgewell/trace-mapping@0.3.25':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@ -2958,6 +3054,11 @@ snapshots:
callsites@3.1.0: {} callsites@3.1.0: {}
camel-case@4.1.2:
dependencies:
pascal-case: 3.1.2
tslib: 2.6.2
camelcase-keys@6.2.2: camelcase-keys@6.2.2:
dependencies: dependencies:
camelcase: 5.3.1 camelcase: 5.3.1
@ -2991,6 +3092,10 @@ snapshots:
isobject: 3.0.1 isobject: 3.0.1
static-extend: 0.1.2 static-extend: 0.1.2
clean-css@5.3.3:
dependencies:
source-map: 0.6.1
cliui@8.0.1: cliui@8.0.1:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
@ -3016,6 +3121,10 @@ snapshots:
colord@2.9.3: {} colord@2.9.3: {}
commander@10.0.1: {}
commander@2.20.3: {}
component-emitter@1.3.0: {} component-emitter@1.3.0: {}
concat-map@0.0.1: {} concat-map@0.0.1: {}
@ -3139,12 +3248,19 @@ snapshots:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
dot-case@3.0.4:
dependencies:
no-case: 3.0.4
tslib: 2.6.2
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
end-of-stream@1.4.4: end-of-stream@1.4.4:
dependencies: dependencies:
once: 1.4.0 once: 1.4.0
entities@4.5.0: {}
error-ex@1.3.2: error-ex@1.3.2:
dependencies: dependencies:
is-arrayish: 0.2.1 is-arrayish: 0.2.1
@ -3733,6 +3849,16 @@ snapshots:
dependencies: dependencies:
lru-cache: 6.0.0 lru-cache: 6.0.0
html-minifier-terser@7.2.0:
dependencies:
camel-case: 4.1.2
clean-css: 5.3.3
commander: 10.0.1
entities: 4.5.0
param-case: 3.0.4
relateurl: 0.2.7
terser: 5.31.0
html-tags@3.3.1: {} html-tags@3.3.1: {}
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
@ -3974,6 +4100,10 @@ snapshots:
lodash.truncate@4.4.2: {} lodash.truncate@4.4.2: {}
lower-case@2.0.2:
dependencies:
tslib: 2.6.2
lru-cache@6.0.0: lru-cache@6.0.0:
dependencies: dependencies:
yallist: 4.0.0 yallist: 4.0.0
@ -4071,6 +4201,11 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
no-case@3.0.4:
dependencies:
lower-case: 2.0.2
tslib: 2.6.2
node-fetch@2.6.7: node-fetch@2.6.7:
dependencies: dependencies:
whatwg-url: 5.0.0 whatwg-url: 5.0.0
@ -4168,6 +4303,11 @@ snapshots:
pako@1.0.11: {} pako@1.0.11: {}
param-case@3.0.4:
dependencies:
dot-case: 3.0.4
tslib: 2.6.2
parent-module@1.0.1: parent-module@1.0.1:
dependencies: dependencies:
callsites: 3.1.0 callsites: 3.1.0
@ -4179,6 +4319,11 @@ snapshots:
json-parse-even-better-errors: 2.3.1 json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4 lines-and-columns: 1.2.4
pascal-case@3.1.2:
dependencies:
no-case: 3.0.4
tslib: 2.6.2
pascalcase@0.1.1: {} pascalcase@0.1.1: {}
path-exists@4.0.0: {} path-exists@4.0.0: {}
@ -4296,6 +4441,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
set-function-name: 2.0.2 set-function-name: 2.0.2
relateurl@0.2.7: {}
require-directory@2.1.1: {} require-directory@2.1.1: {}
require-from-string@2.0.2: {} require-from-string@2.0.2: {}
@ -4605,6 +4752,13 @@ snapshots:
inherits: 2.0.4 inherits: 2.0.4
readable-stream: 3.6.2 readable-stream: 3.6.2
terser@5.31.0:
dependencies:
'@jridgewell/source-map': 0.3.6
acorn: 8.10.0
commander: 2.20.3
source-map-support: 0.5.21
text-table@0.2.0: {} text-table@0.2.0: {}
through@2.3.8: {} through@2.3.8: {}
@ -4646,6 +4800,8 @@ snapshots:
tslib@1.14.1: {} tslib@1.14.1: {}
tslib@2.6.2: {}
tsutils@3.21.0(typescript@5.4.5): tsutils@3.21.0(typescript@5.4.5):
dependencies: dependencies:
tslib: 1.14.1 tslib: 1.14.1

View file

@ -20,8 +20,10 @@ import "../suppressExperimentalWarnings.js";
import "../checkNodeVersion.js"; import "../checkNodeVersion.js";
import { exec, execSync } from "child_process"; import { exec, execSync } from "child_process";
import esbuild from "esbuild";
import { constants as FsConstants, readFileSync } from "fs"; import { constants as FsConstants, readFileSync } from "fs";
import { access, readdir, readFile } from "fs/promises"; import { access, readdir, readFile } from "fs/promises";
import { minify as minifyHtml } from "html-minifier-terser";
import { join, relative } from "path"; import { join, relative } from "path";
import { promisify } from "util"; import { promisify } from "util";
@ -161,21 +163,60 @@ export const gitRemotePlugin = {
/** /**
* @type {import("esbuild").Plugin} * @type {import("esbuild").Plugin}
*/ */
export const fileIncludePlugin = { export const fileUrlPlugin = {
name: "file-include-plugin", name: "file-uri-plugin",
setup: build => { setup: build => {
const filter = /^~fileContent\/.+$/; const filter = /^file:\/\/.+$/;
build.onResolve({ filter }, args => ({ build.onResolve({ filter }, args => ({
namespace: "include-file", namespace: "file-uri",
path: args.path, path: args.path,
pluginData: { pluginData: {
path: join(args.resolveDir, args.path.slice("include-file/".length)) uri: args.path,
path: join(args.resolveDir, args.path.slice("file://".length).split("?")[0])
} }
})); }));
build.onLoad({ filter, namespace: "include-file" }, async ({ pluginData: { path } }) => { build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => {
const [name, format] = path.split(";"); const { searchParams } = new URL(uri);
const base64 = searchParams.has("base64");
const minify = isStandalone === "true" && searchParams.has("minify");
const noTrim = searchParams.get("trim") === "false";
const encoding = base64 ? "base64" : "utf-8";
let content;
if (!minify) {
content = await readFile(path, encoding);
if (!noTrim) content = content.trimEnd();
} else {
if (path.endsWith(".html")) {
content = await minifyHtml(await readFile(path, "utf-8"), {
collapseWhitespace: true,
removeComments: true,
minifyCSS: true,
minifyJS: true,
removeEmptyAttributes: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true
});
} else if (/[mc]?[jt]sx?$/.test(path)) {
const res = await esbuild.build({
entryPoints: [path],
write: false,
minify: true
});
content = res.outputFiles[0].text;
} else {
throw new Error(`Don't know how to minify file type: ${path}`);
}
if (base64)
content = Buffer.from(content).toString("base64");
}
return { return {
contents: `export default ${JSON.stringify(await readFile(name, format ?? "utf-8"))}` contents: `export default ${JSON.stringify(content)}`
}; };
}); });
} }
@ -217,7 +258,7 @@ export const commonOpts = {
sourcemap: watch ? "inline" : "", sourcemap: watch ? "inline" : "",
legalComments: "linked", legalComments: "linked",
banner, banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin], plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"], external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"], inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement", jsxFactory: "VencordCreateElement",

View file

@ -243,19 +243,27 @@ page.on("console", async e => {
} }
} }
if (isDebug) { async function getText() {
console.error(e.text());
} else if (level === "error") {
const text = await Promise.all(
e.args().map(async a => {
try { try {
return await Promise.all(
e.args().map(async a => {
return await maybeGetError(a) || await a.jsonValue(); return await maybeGetError(a) || await a.jsonValue();
} catch (e) {
return a.toString();
}
}) })
).then(a => a.join(" ").trim()); ).then(a => a.join(" ").trim());
} catch {
return e.text();
}
}
if (isDebug) {
const text = await getText();
console.error(text);
if (text.includes("A fatal error occurred:")) {
process.exit(1);
}
} else if (level === "error") {
const text = await getText();
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) { if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
console.error("[Unexpected Error]", text); console.error("[Unexpected Error]", text);
@ -322,22 +330,31 @@ async function runtime(token: string) {
const validChunks = new Set<string>(); const validChunks = new Set<string>();
const invalidChunks = new Set<string>(); const invalidChunks = new Set<string>();
const deferredRequires = new Set<string>();
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void; let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r); const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
// True if resolved, false otherwise // True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>; const chunksSearchPromises = [] as Array<() => boolean>;
const lazyChunkRegex = canonicalizeMatch(/Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/g);
const chunkIdsRegex = canonicalizeMatch(/\("(.+?)"\)/g); const LazyChunkRegex = canonicalizeMatch(/(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\)))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
async function searchAndLoadLazyChunks(factoryCode: string) { async function searchAndLoadLazyChunks(factoryCode: string) {
const lazyChunks = factoryCode.matchAll(lazyChunkRegex); const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
const chunkIds = Array.from(rawChunkIds.matchAll(chunkIdsRegex)).map(m => m[1]); // the chunk containing the component
if (chunkIds.length === 0) return; const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIdsArray, rawChunkIdsSingle, entryPoint]) => {
const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Vencord.Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
if (chunkIds.length === 0) {
return;
}
let invalidChunkGroup = false; let invalidChunkGroup = false;
@ -373,6 +390,11 @@ async function runtime(token: string) {
// Requires the entry points for all valid chunk groups // Requires the entry points for all valid chunk groups
for (const [, entryPoint] of validChunkGroups) { for (const [, entryPoint] of validChunkGroups) {
try { try {
if (shouldForceDefer) {
deferredRequires.add(entryPoint);
continue;
}
if (wreq.m[entryPoint]) wreq(entryPoint as any); if (wreq.m[entryPoint]) wreq(entryPoint as any);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -435,6 +457,11 @@ async function runtime(token: string) {
await chunksSearchingDone; await chunksSearchingDone;
// Require deferred entry points
for (const deferredRequire of deferredRequires) {
wreq!(deferredRequire as any);
}
// All chunks Discord has mapped to asset files, even if they are not used anymore // All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as string[]; const allChunks = [] as string[];
@ -514,7 +541,6 @@ async function runtime(token: string) {
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000); setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
} catch (e) { } catch (e) {
console.log("[PUP_DEBUG]", "A fatal error occurred:", e); console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
process.exit(1);
} }
} }

View file

@ -23,12 +23,11 @@ import "./settings";
import { debounce } from "@shared/debounce"; import { debounce } from "@shared/debounce";
import { IpcEvents } from "@shared/IpcEvents"; import { IpcEvents } from "@shared/IpcEvents";
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron"; import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
import monacoHtml from "file://monacoWin.html?minify&base64";
import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs"; import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
import { open, readdir, readFile } from "fs/promises"; import { open, readdir, readFile } from "fs/promises";
import { join, normalize } from "path"; import { join, normalize } from "path";
import monacoHtml from "~fileContent/monacoWin.html;base64";
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes"; import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants"; import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
import { makeLinksOpenExternally } from "./utils/externalLinks"; import { makeLinksOpenExternally } from "./utils/externalLinks";

View file

@ -73,6 +73,9 @@ if (!IS_VANILLA) {
const original = options.webPreferences.preload; const original = options.webPreferences.preload;
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js"); options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
options.webPreferences.sandbox = false; options.webPreferences.sandbox = false;
// work around discord unloading when in background
options.webPreferences.backgroundThrottling = false;
if (settings.frameless) { if (settings.frameless) {
options.frame = false; options.frame = false;
} else if (process.platform === "win32" && settings.winNativeTitleBar) { } else if (process.platform === "win32" && settings.winNativeTitleBar) {
@ -136,6 +139,15 @@ if (!IS_VANILLA) {
} }
return originalAppend.apply(this, args); return originalAppend.apply(this, args);
}; };
// disable renderer backgrounding to prevent the app from unloading when in the background
// https://github.com/electron/electron/issues/2822
// https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
// Work around discord unloading when in background
// Discord also recently started adding these flags but only on windows for some reason dunno why, it happens on Linux too
app.commandLine.appendSwitch("disable-renderer-backgrounding");
app.commandLine.appendSwitch("disable-background-timer-throttling");
app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
} else { } else {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord"); console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
} }

2
src/modules.d.ts vendored
View file

@ -38,7 +38,7 @@ declare module "~git-remote" {
export default remote; export default remote;
} }
declare module "~fileContent/*" { declare module "file://*" {
const content: string; const content: string;
export default content; export default content;
} }

View file

@ -29,7 +29,7 @@ export default definePlugin({
find: '"NoticeStore"', find: '"NoticeStore"',
replacement: [ replacement: [
{ {
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g, match: /(?<=!1;)\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
replace: "if(Vencord.Api.Notices.currentNotice)return false;$&" replace: "if(Vencord.Api.Notices.currentNotice)return false;$&"
}, },
{ {

View file

@ -56,6 +56,26 @@ export default definePlugin({
} }
] ]
}, },
// Discord Stable
// FIXME: remove once change merged to stable
{
find: "Messages.ACTIVITY_SETTINGS",
replacement: {
get match() {
switch (Settings.plugins.Settings.settingsLocation) {
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/;
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/;
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/;
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
case "aboveActivity":
default:
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/;
}
},
replace: "...$self.makeSettingsCategories($1),$&"
}
},
{ {
find: "Messages.ACTIVITY_SETTINGS", find: "Messages.ACTIVITY_SETTINGS",
replacement: { replacement: {

View file

@ -31,10 +31,10 @@ export default definePlugin({
// Some modules match the find but the replacement is returned untouched // Some modules match the find but the replacement is returned untouched
noWarn: true, noWarn: true,
replacement: { replacement: {
match: /canAnimate:.+?(?=([,}].*?\)))/g, match: /canAnimate:.+?([,}].*?\))/g,
replace: (m, rest) => { replace: (m, rest) => {
const destructuringMatch = rest.match(/}=.+/); const destructuringMatch = rest.match(/}=.+/);
if (destructuringMatch == null) return "canAnimate:!0"; if (destructuringMatch == null) return `canAnimate:!0${rest}`;
return m; return m;
} }
} }

View file

@ -73,13 +73,13 @@ export default definePlugin({
{ {
find: "instantBatchUpload:function", find: "instantBatchUpload:function",
replacement: { replacement: {
match: /uploadFiles:(.{1,2}),/, match: /uploadFiles:(\i),/,
replace: replace:
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),", "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
}, },
}, },
{ {
find: "message.attachments", find: 'addFilesTo:"message.attachments"',
replacement: { replacement: {
match: /(\i.uploadFiles\((\i),)/, match: /(\i.uploadFiles\((\i),)/,
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1" replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"

View file

@ -112,8 +112,8 @@ export default definePlugin({
replacement: [ replacement: [
// Create the isBetterFolders variable in the GuildsBar component // Create the isBetterFolders variable in the GuildsBar component
{ {
match: /(?<=let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?)(?=}=\i,)/, match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i,)/,
replace: ",isBetterFolders" replace: "$&,isBetterFolders"
}, },
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders // If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
{ {

View file

@ -1,6 +1,6 @@
# BetterRoleContext # BetterRoleContext
Adds options to copy role color and edit role when right clicking roles in the user profile Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile
![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326) ![](https://github.com/Vendicated/Vencord/assets/45497981/354220a4-09f3-4c5f-a28e-4b19ca775190)

View file

@ -4,9 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { definePluginSettings } from "@api/Settings";
import { ImageIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getCurrentGuild } from "@utils/discord"; import { getCurrentGuild, openImageModal } from "@utils/discord";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common"; import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
@ -34,10 +36,34 @@ function AppearanceIcon() {
); );
} }
const settings = definePluginSettings({
roleIconFileFormat: {
type: OptionType.SELECT,
description: "File format to use when viewing role icons",
options: [
{
label: "png",
value: "png",
default: true
},
{
label: "webp",
value: "webp",
},
{
label: "jpg",
value: "jpg"
}
]
}
});
export default definePlugin({ export default definePlugin({
name: "BetterRoleContext", name: "BetterRoleContext",
description: "Adds options to copy role color / edit role when right clicking roles in the user profile", description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
authors: [Devs.Ven], authors: [Devs.Ven, Devs.goodbee],
settings,
start() { start() {
// DeveloperMode needs to be enabled for the context menu to be shown // DeveloperMode needs to be enabled for the context menu to be shown
@ -63,6 +89,20 @@ export default definePlugin({
); );
} }
if (role.icon) {
children.push(
<Menu.MenuItem
id="vc-view-role-icon"
label="View Role Icon"
action={() => {
openImageModal(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`);
}}
icon={ImageIcon}
/>
);
}
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) { if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.push( children.push(
<Menu.MenuItem <Menu.MenuItem

View file

@ -111,8 +111,8 @@ export default definePlugin({
{ // Load menu TOC eagerly { // Load menu TOC eagerly
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format", find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
replacement: { replacement: {
match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/, match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/,
replace: "(async ()=>$2)()," replace: "$&(async ()=>$2)(),"
}, },
predicate: () => settings.store.eagerLoad predicate: () => settings.store.eagerLoad
}, },

View file

@ -34,9 +34,9 @@ export default definePlugin({
{ {
find: ".AVATAR_STATUS_MOBILE_16;", find: ".AVATAR_STATUS_MOBILE_16;",
replacement: { replacement: {
match: /(?<=fromIsMobile:\i=!0,.+?)status:(\i)/, match: /(fromIsMobile:\i=!0,.+?)status:(\i)/,
// Rename field to force it to always use "online" // Rename field to force it to always use "online"
replace: 'status_$:$1="online"' replace: '$1status_$:$2="online"'
} }
} }
] ]

View file

@ -17,24 +17,33 @@
*/ */
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getCurrentChannel, getCurrentGuild } from "@utils/discord";
import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy";
import { relaunch } from "@utils/native"; import { relaunch } from "@utils/native";
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches"; import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
import definePlugin from "@utils/types"; import definePlugin, { PluginNative, StartAt } from "@utils/types";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { extract, filters, findAll, search } from "@webpack"; import { extract, filters, findAll, findModuleId, search } from "@webpack";
import { React, ReactDOM } from "@webpack/common"; import * as Common from "@webpack/common";
import type { ComponentType } from "react"; import type { ComponentType } from "react";
const WEB_ONLY = (f: string) => () => { const DESKTOP_ONLY = (f: string) => () => {
throw new Error(`'${f}' is Discord Desktop only.`); throw new Error(`'${f}' is Discord Desktop only.`);
}; };
export default definePlugin({ const define: typeof Object.defineProperty =
name: "ConsoleShortcuts", (obj, prop, desc) => {
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.", if (Object.hasOwn(desc, "value"))
authors: [Devs.Ven], desc.writable = true;
getShortcuts() { return Object.defineProperty(obj, prop, {
configurable: true,
enumerable: true,
...desc
});
};
function makeShortcuts() {
function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) { function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) {
const cache = new Map<string, unknown>(); const cache = new Map<string, unknown>();
@ -64,16 +73,17 @@ export default definePlugin({
let fakeRenderWin: WeakRef<Window> | undefined; let fakeRenderWin: WeakRef<Window> | undefined;
const find = newFindWrapper(f => f); const find = newFindWrapper(f => f);
const findByProps = newFindWrapper(filters.byProps); const findByProps = newFindWrapper(filters.byProps);
return { return {
...Vencord.Webpack.Common, ...Object.fromEntries(Object.keys(Common).map(key => [key, { getter: () => Common[key] }])),
wp: Vencord.Webpack, wp: Webpack,
wpc: Webpack.wreq.c, wpc: { getter: () => Webpack.cache },
wreq: Webpack.wreq, wreq: { getter: () => Webpack.wreq },
wpsearch: search, wpsearch: search,
wpex: extract, wpex: extract,
wpexs: (code: string) => extract(Webpack.findModuleId(code)!), wpexs: (code: string) => extract(findModuleId(code)!),
find, find,
findAll, findAll: findAll,
findByProps, findByProps,
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)), findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
findByCode: newFindWrapper(filters.byCode), findByCode: newFindWrapper(filters.byCode),
@ -82,18 +92,21 @@ export default definePlugin({
findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)), findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)),
findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]], findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]],
findStore: newFindWrapper(filters.byStoreName), findStore: newFindWrapper(filters.byStoreName),
PluginsApi: Vencord.Plugins, PluginsApi: { getter: () => Vencord.Plugins },
plugins: Vencord.Plugins.plugins, plugins: { getter: () => Vencord.Plugins.plugins },
Settings: Vencord.Settings, Settings: { getter: () => Vencord.Settings },
Api: Vencord.Api, Api: { getter: () => Vencord.Api },
Util: { getter: () => Vencord.Util },
reload: () => location.reload(), reload: () => location.reload(),
restart: IS_WEB ? WEB_ONLY("restart") : relaunch, restart: IS_WEB ? DESKTOP_ONLY("restart") : relaunch,
canonicalizeMatch, canonicalizeMatch,
canonicalizeReplace, canonicalizeReplace,
canonicalizeReplacement, canonicalizeReplacement,
fakeRender: (component: ComponentType, props: any) => { fakeRender: (component: ComponentType, props: any) => {
const prevWin = fakeRenderWin?.deref(); const prevWin = fakeRenderWin?.deref();
const win = prevWin?.closed === false ? prevWin : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!; const win = prevWin?.closed === false
? prevWin
: window.open("about:blank", "Fake Render", "popup,width=500,height=500")!;
fakeRenderWin = new WeakRef(win); fakeRenderWin = new WeakRef(win);
win.focus(); win.focus();
@ -115,21 +128,88 @@ export default definePlugin({
}); });
} }
ReactDOM.render(React.createElement(component, props), doc.body.appendChild(document.createElement("div"))); Common.ReactDOM.render(Common.React.createElement(component, props), doc.body.appendChild(document.createElement("div")));
}
};
}, },
preEnable: (plugin: string) => (Vencord.Settings.plugins[plugin] ??= { enabled: true }).enabled = true,
channel: { getter: () => getCurrentChannel(), preload: false },
channelId: { getter: () => Common.SelectedChannelStore.getChannelId(), preload: false },
guild: { getter: () => getCurrentGuild(), preload: false },
guildId: { getter: () => Common.SelectedGuildStore.getGuildId(), preload: false },
me: { getter: () => Common.UserStore.getCurrentUser(), preload: false },
meId: { getter: () => Common.UserStore.getCurrentUser().id, preload: false },
messages: { getter: () => Common.MessageStore.getMessages(Common.SelectedChannelStore.getChannelId()), preload: false }
};
}
function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
const currentVal = val.getter();
if (!currentVal || val.preload === false) return currentVal;
const value = currentVal[SYM_LAZY_GET]
? forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED]
: currentVal;
if (value) define(window.shortcutList, key, { value });
return value;
}
export default definePlugin({
name: "ConsoleShortcuts",
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.",
authors: [Devs.Ven],
startAt: StartAt.Init,
start() { start() {
const shortcuts = this.getShortcuts(); const shortcuts = makeShortcuts();
window.shortcutList = shortcuts; window.shortcutList = {};
for (const [key, val] of Object.entries(shortcuts))
for (const [key, val] of Object.entries(shortcuts)) {
if ("getter" in val) {
define(window.shortcutList, key, {
get: () => loadAndCacheShortcut(key, val, true)
});
define(window, key, {
get: () => window.shortcutList[key]
});
} else {
window.shortcutList[key] = val;
window[key] = val; window[key] = val;
}
}
// unproxy loaded modules
Webpack.onceReady.then(() => {
setTimeout(() => this.eagerLoad(false), 1000);
if (!IS_WEB) {
const Native = VencordNative.pluginHelpers.ConsoleShortcuts as PluginNative<typeof import("./native")>;
Native.initDevtoolsOpenEagerLoad();
}
});
},
async eagerLoad(forceLoad: boolean) {
await Webpack.onceReady;
const shortcuts = makeShortcuts();
for (const [key, val] of Object.entries(shortcuts)) {
if (!Object.hasOwn(val, "getter") || (val as any).preload === false) continue;
try {
loadAndCacheShortcut(key, val, forceLoad);
} catch { } // swallow not found errors in DEV
}
}, },
stop() { stop() {
delete window.shortcutList; delete window.shortcutList;
for (const key in this.getShortcuts()) for (const key in makeShortcuts()) {
delete window[key]; delete window[key];
} }
}
}); });

View file

@ -0,0 +1,16 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { IpcMainInvokeEvent } from "electron";
export function initDevtoolsOpenEagerLoad(e: IpcMainInvokeEvent) {
const handleDevtoolsOpened = () => e.sender.executeJavaScript("Vencord.Plugins.plugins.ConsoleShortcuts.eagerLoad(true)");
if (e.sender.isDevToolsOpened())
handleDevtoolsOpened();
else
e.sender.once("devtools-opened", () => handleDevtoolsOpened());
}

View file

@ -18,7 +18,7 @@ export default definePlugin({
type: OptionType.SELECT, type: OptionType.SELECT,
options: [ options: [
{ {
label: "Ctrl+Enter (Enter or Shift+Enter for new line)", label: "Ctrl+Enter (Enter or Shift+Enter for new line) (cmd+enter on macOS)",
value: "ctrl+enter" value: "ctrl+enter"
}, },
{ {
@ -54,7 +54,7 @@ export default definePlugin({
result = event.shiftKey; result = event.shiftKey;
break; break;
case "ctrl+enter": case "ctrl+enter":
result = event.ctrlKey; result = navigator.platform.includes("Mac") ? event.metaKey : event.ctrlKey;
break; break;
case "enter": case "enter":
result = !event.shiftKey && !event.ctrlKey; result = !event.shiftKey && !event.ctrlKey;

View file

@ -182,8 +182,8 @@ export default definePlugin({
// add dearrow button // add dearrow button
{ {
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/, match: /children:\[(?=null!=\i\?(\i)\.renderSuppressButton)/,
replace: "children:[$self.renderButton(this),", replace: "children:[$self.renderButton($1),",
predicate: () => !settings.store.hideButton predicate: () => !settings.store.hideButton
} }
] ]

View file

@ -0,0 +1,35 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { moment } from "@webpack/common";
export default definePlugin({
name: "DontRoundMyTimestamps",
authors: [Devs.Lexi],
description: "Always rounds relative timestamps down, so 7.6y becomes 7y instead of 8y",
start() {
moment.relativeTimeRounding(Math.floor);
},
stop() {
moment.relativeTimeRounding(Math.round);
}
});

View file

@ -344,8 +344,8 @@ export default definePlugin({
{ {
// Patch the stickers array to add fake nitro stickers // Patch the stickers array to add fake nitro stickers
predicate: () => settings.store.transformStickers, predicate: () => settings.store.transformStickers,
match: /(?<=renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;)/, match: /renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;/,
replace: (_, message, stickers) => `${stickers}=$self.patchFakeNitroStickers(${stickers},${message});` replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`
}, },
{ {
// Filter attachments to remove fake nitro stickers or emojis // Filter attachments to remove fake nitro stickers or emojis
@ -813,7 +813,7 @@ export default definePlugin({
}, },
canUseEmote(e: Emoji, channelId: string) { canUseEmote(e: Emoji, channelId: string) {
if (e.type === "UNICODE") return true; if (e.type === 0) return true;
if (e.available === false) return false; if (e.available === false) return false;
const isUnusableRoleSubEmoji = RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji; const isUnusableRoleSubEmoji = RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;

View file

@ -0,0 +1,3 @@
.vc-fpt-preview * {
pointer-events: none;
}

View file

@ -17,13 +17,17 @@
*/ */
// This plugin is a port from Alyxia's Vendetta plugin // This plugin is a port from Alyxia's Vendetta plugin
import "./index.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc"; import { classes, copyWithToast } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Button, Forms } from "@webpack/common"; import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import virtualMerge from "virtual-merge"; import virtualMerge from "virtual-merge";
@ -81,6 +85,34 @@ const settings = definePluginSettings({
} }
}); });
interface ColorPickerProps {
color: number | null;
label: React.ReactElement;
showEyeDropper?: boolean;
suggestedColors?: string[];
onChange(value: number | null): void;
}
// I can't be bothered to figure out the semantics of this component. The
// functions surely get some event argument sent to them and they likely aren't
// all required. If anyone who wants to use this component stumbles across this
// code, you'll have to do the research yourself.
interface ProfileModalProps {
user: User;
pendingThemeColors: [number, number];
onAvatarChange: () => void;
onBannerChange: () => void;
canUsePremiumCustomization: boolean;
hideExampleButton: boolean;
hideFakeActivity: boolean;
isTryItOutFlow: boolean;
}
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>('"ProfileCustomizationPreview"');
const requireColorPicker = extractAndLoadChunksLazy(["USER_SETTINGS_PROFILE_COLOR_DEFAULT_BUTTON.format"], /createPromise:\(\)=>\i\.\i\("(.+?)"\).then\(\i\.bind\(\i,"(.+?)"\)\)/);
export default definePlugin({ export default definePlugin({
name: "FakeProfileThemes", name: "FakeProfileThemes",
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding", description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
@ -101,21 +133,98 @@ export default definePlugin({
} }
} }
], ],
settingsAboutComponent: () => ( settingsAboutComponent: () => {
const existingColors = decode(
UserProfileStore.getUserProfile(UserStore.getCurrentUser().id).bio
) ?? [0, 0];
const [color1, setColor1] = useState(existingColors[0]);
const [color2, setColor2] = useState(existingColors[1]);
const [, , loadingColorPickerChunk] = useAwaiter(requireColorPicker);
return (
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle> <Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
<Forms.FormText> <Forms.FormText>
After enabling this plugin, you will see custom colors in the profiles of other people using compatible plugins. <br /> After enabling this plugin, you will see custom colors in
the profiles of other people using compatible plugins.{" "}
<br />
To set your own colors: To set your own colors:
<ul> <ul>
<li> go to your profile settings</li> <li>
<li> choose your own colors in the Nitro preview</li> use the color pickers below to choose your colors
</li>
<li> click the "Copy 3y3" button</li> <li> click the "Copy 3y3" button</li>
<li> paste the invisible text anywhere in your bio</li> <li> paste the invisible text anywhere in your bio</li>
</ul><br /> </ul><br />
<b>Please note:</b> if you are using a theme which hides nitro ads, you should disable it temporarily to set colors. <Forms.FormDivider
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
{!loadingColorPickerChunk && (
<Flex
direction={Flex.Direction.HORIZONTAL}
style={{ gap: "1rem" }}
>
<ColorPicker
color={color1}
label={
<Text
variant={"text-xs/normal"}
style={{ marginTop: "4px" }}
>
Primary
</Text>
}
onChange={(color: number) => {
setColor1(color);
}}
/>
<ColorPicker
color={color2}
label={
<Text
variant={"text-xs/normal"}
style={{ marginTop: "4px" }}
>
Accent
</Text>
}
onChange={(color: number) => {
setColor2(color);
}}
/>
<Button
onClick={() => {
const colorString = encode(color1, color2);
copyWithToast(colorString);
}}
color={Button.Colors.PRIMARY}
size={Button.Sizes.XLARGE}
>
Copy 3y3
</Button>
</Flex>
)}
<Forms.FormDivider
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Preview</Forms.FormTitle>
<div className="vc-fpt-preview">
<ProfileModal
user={UserStore.getCurrentUser()}
pendingThemeColors={[color1, color2]}
onAvatarChange={() => { }}
onBannerChange={() => { }}
canUsePremiumCustomization={true}
hideExampleButton={true}
hideFakeActivity={true}
isTryItOutFlow={true}
/>
</div>
</Forms.FormText> </Forms.FormText>
</Forms.FormSection>), </Forms.FormSection>);
},
settings, settings,
colorDecodeHook(user: UserProfile) { colorDecodeHook(user: UserProfile) {
if (user) { if (user) {

View file

@ -52,6 +52,8 @@ export default definePlugin({
getFriendSince(userId: string) { getFriendSince(userId: string) {
try { try {
if (!RelationshipStore.isFriend(userId)) return null;
return RelationshipStore.getSince(userId); return RelationshipStore.getSince(userId);
} catch (err) { } catch (err) {
new Logger("FriendsSince").error(err); new Logger("FriendsSince").error(err);
@ -60,6 +62,8 @@ export default definePlugin({
}, },
friendsSince: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => { friendsSince: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => {
if (!RelationshipStore.isFriend(userId)) return null;
const friendsSince = RelationshipStore.getSince(userId); const friendsSince = RelationshipStore.getSince(userId);
if (!friendsSince) return null; if (!friendsSince) return null;

View file

@ -228,15 +228,15 @@ export default definePlugin({
{ {
find: ".activityTitleText,variant", find: ".activityTitleText,variant",
replacement: { replacement: {
match: /(?<=\i\.activityTitleText.+?children:(\i)\.name.*?}\),)/, match: /\.activityTitleText.+?children:(\i)\.name.*?}\),/,
replace: (_, props) => `$self.renderToggleActivityButton(${props}),` replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
}, },
}, },
{ {
find: ".activityCardDetails,children", find: ".activityCardDetails,children",
replacement: { replacement: {
match: /(?<=\i\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),)/, match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/,
replace: (_, props) => `$self.renderToggleActivityButton(${props}),` replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
} }
} }
], ],

View file

@ -67,15 +67,18 @@ export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: i
} }
}; };
const syncVideos = () => { const syncVideos = () => {
currentVideoElementRef.current!.currentTime = originalVideoElementRef.current!.currentTime; if (currentVideoElementRef.current && originalVideoElementRef.current)
currentVideoElementRef.current.currentTime = originalVideoElementRef.current.currentTime;
}; };
const updateMousePosition = (e: MouseEvent) => { const updateMousePosition = (e: MouseEvent) => {
if (!element.current) return;
if (instance.state.mouseOver && instance.state.mouseDown) { if (instance.state.mouseOver && instance.state.mouseDown) {
const offset = size.current / 2; const offset = size.current / 2;
const pos = { x: e.pageX, y: e.pageY }; const pos = { x: e.pageX, y: e.pageY };
const x = -((pos.x - element.current!.getBoundingClientRect().left) * zoom.current - offset); const x = -((pos.x - element.current.getBoundingClientRect().left) * zoom.current - offset);
const y = -((pos.y - element.current!.getBoundingClientRect().top) * zoom.current - offset); const y = -((pos.y - element.current.getBoundingClientRect().top) * zoom.current - offset);
setLensPosition({ x: e.x - offset, y: e.y - offset }); setLensPosition({ x: e.x - offset, y: e.y - offset });
setImagePosition({ x, y }); setImagePosition({ x, y });
setOpacity(1); setOpacity(1);
@ -184,6 +187,7 @@ export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: i
src={originalVideoElementRef.current?.src ?? instance.props.src} src={originalVideoElementRef.current?.src ?? instance.props.src}
autoPlay autoPlay
loop loop
muted
/> />
) : ( ) : (
<img <img

View file

@ -18,88 +18,82 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import presetQuotesText from "file://quotes.txt";
// These are Xor encrypted to prevent you from spoiling yourself when you read the source code. const presetQuotes = presetQuotesText.split("\n").map(quote => /^\s*[^#\s]/.test(quote) && quote.trim()).filter(Boolean) as string[];
// don't worry about it :P const noQuotesQuote = "Did you really disable all loading quotes? What a buffoon you are...";
const quotes = [
"Eyrokac",
"Rdcg$l`'k|~n",
'H`tf$d&iajo+d`{"',
"Sucqplh`(Eclhualva()&",
"Lncgmka'8KNMDC,shpanf'`x./,",
"Ioqweijnfn*IeuvfvAotkfxo./,",
'Hd{#cp\x7Ft$)nbd!{lq%mig~*\x7Fh`v#mk&sm{gx nd#idjb(a\x7Ffao"bja&amdkge!Rloìkhf)hyedfjjb*\'^hzdrdmm$lu\'|ao+mnqw$fijxh~bbmg#Tjmîefd+fnp#lpkffz5',
"h",
"sijklm&cam*rot\"hjjq'|ak\x7F xmv#wc'ep*mawmvvlrb(|ynr>\"Aqq&cgg-\x7F ugoh%rom)e\x7Fhdpp%$",
'Tnfb}"u\'~`nno!kp$vvhfzeyee"a}%Tfam*Xh`fls%Jboldos-"lj`&hn)~ce!`jcbct|)gdbhnf$wikm$zgaxkmc%afely+og"144?\'ign+iu%p$qisiefr gpfa$',
"Ndtfv%ahfgk+ghtf$|ir(|z' Oguaw&`ggdj mgw$|ir(me|n",
"(!ͣ³$͙ʐ'ͩ¹#",
"(ネ◗ロ◑,マ-2ャユ✬",
"Ynw#hjil(ze+psgwp|&sgmkr!",
"Tikmolh`(fl+a!dvjk\x7F'y|e\x7Fe/,-",
"3/3750?5><9>885:7",
"mdmt",
"Wdn`khc+(oxbeof",
'Ig"zkp*\'g{*xolglj`&~g|*gowg/$mgt(Eclm`.#ticf{l*xed"wl`&Kangj igbhqn\'d`dn `v#lqrw{3%$bhv-h|)kangj_imwhlhb',
"Tscmw%Tnoa~x",
"If#npus(ec`e!vl$lhsm{`ncu\"ekw&f(defeov-$Rnf|)sdupf$wcam{ceg!vl$du'D`d~x-\"jw%oi(okht-\"DJP)Kags,!mq$du'A|n sg`akrkq)~jkdl#pj&diefbnf\"jp)&@F\\*{ltq#Hlhrp'",
"Ynw$v`&cg`dl fml`%rhlhs*",
"Dnl$p%qhz{s' hv$w%hh|aceg!;#gpvt(fl+cndea`&dg|fon&v#wjjqm(",
"\ud83d)pft`gs(ec`e!13$qojmz#",
"a!njcmr'ide~nu\"lb%rheoedldpz$lu'gbkr",
"dn\"zkp&kgo4",
"hnpqkw",
"sn\"fau",
"Sn\"tmqnh}}*musvkaw&flf&+ldv$w%lr{}*aulr#vlao|)cetn\"jp$",
"Dxkmc%ot(hhxomwwai'{hln",
"hd{#}js&(pe~'sg#gprb(3#\"",
"hd{b${",
"<;vqkijbq33271:56<3799?24944:",
"Thof$lu'ofdn,!qsefc'az*bnrcma+&Om{o+iu\"`khct$)bnrd\"bcdoi&",
"snofplkb{)c'r\"lod'|f*aurv#cpno`abchijklmno",
"Wdn`khc'|f*eghl{%"
];
const settings = definePluginSettings({ const settings = definePluginSettings({
replaceEvents: { replaceEvents: {
description: "Replace Event Quotes too", description: "Should this plugin also apply during events with special event themed quotes? (e.g. Halloween)",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true default: true
} },
enablePluginPresetQuotes: {
description: "Enable the quotes preset by this plugin",
type: OptionType.BOOLEAN,
default: true
},
enableDiscordPresetQuotes: {
description: "Enable Discord's preset quotes (including event quotes, during events)",
type: OptionType.BOOLEAN,
default: false
},
additionalQuotes: {
description: "Additional custom quotes to possibly appear, separated by the below delimiter",
type: OptionType.STRING,
default: "",
},
additionalQuotesDelimiter: {
description: "Delimiter for additional quotes",
type: OptionType.STRING,
default: "|",
},
}); });
export default definePlugin({ export default definePlugin({
name: "LoadingQuotes", name: "LoadingQuotes",
description: "Replace Discords loading quotes", description: "Replace Discords loading quotes",
authors: [Devs.Ven, Devs.KraXen72], authors: [Devs.Ven, Devs.KraXen72, Devs.UlyssesZhan],
settings, settings,
patches: [ patches: [
{ {
find: ".LOADING_DID_YOU_KNOW}", find: ".LOADING_DID_YOU_KNOW",
replacement: [ replacement: [
{ {
match: /"_loadingText",function\(\)\{/, match: /"_loadingText".+?(?=(\i)\[.{0,10}\.random)/,
replace: "$&return $self.quote;", replace: "$&$self.mutateQuotes($1),"
}, },
{ {
match: /"_eventLoadingText",function\(\)\{/, match: /"_eventLoadingText".+?(?=(\i)\[.{0,10}\.random)/,
replace: "$&return $self.quote;", replace: "$&$self.mutateQuotes($1),",
predicate: () => settings.store.replaceEvents predicate: () => settings.store.replaceEvents
} }
], ]
}, },
], ],
xor(quote: string) { mutateQuotes(quotes: string[]) {
const key = "read if cute"; try {
const codes = Array.from(quote, (s, i) => s.charCodeAt(0) ^ (i % key.length)); const { enableDiscordPresetQuotes, additionalQuotes, additionalQuotesDelimiter, enablePluginPresetQuotes } = settings.store;
return String.fromCharCode(...codes);
},
get quote() { if (!enableDiscordPresetQuotes)
return this.xor(quotes[Math.floor(Math.random() * quotes.length)]); quotes.length = 0;
if (enablePluginPresetQuotes)
quotes.push(...presetQuotes);
quotes.push(...additionalQuotes.split(additionalQuotesDelimiter).filter(Boolean));
if (!quotes.length)
quotes.push(noQuotesQuote);
} catch (e) {
new Logger("LoadingQuotes").error("Failed to mutate quotes", e);
}
} }
}); });

View file

@ -0,0 +1,37 @@
# Blank lines and lines starting with "#" are ignored
Explode
Read if cute
Have a nice day!
Starting Lightcord...
Loading 0BDFDB.plugin.js...
Installing BetterDiscord...
h
shhhhh did you know that you're my favourite user? But don't tell the others!!
Today's video is sponsored by Raid Shadow Legends, one of the biggest mobile role-playing games of 2019 and it's totally free!
Never gonna give you up, Never gonna let you down
( ͡° ͜ʖ ͡°)
(ノ◕ヮ◕)ノ*:・゚✧
You look so pretty today!
Thinking of a funny quote...
3.141592653589793
meow
Welcome, friend
If you, or someone you love, has Ligma, please see the Ligma health line at https://bit.ly/ligma_hotline
Trans Rights
Id just like to interject for a moment. What youre refering to as Linux, is in fact, GNU/Linux, or as Ive recently taken to calling it, GNU plus Linux.
You're doing good today!
Don't worry, it's nothing 9 cups of coffee couldn't solve!
<EFBFBD>(repeat like 30 times)
a light amount of tomfoolery is okay
do you love?
horror
so eepy
So without further ado, let's just jump right into it!
Dying is absolutely safe
hey you! you're cute :))
heya ~
<:trolley:997086295010594867>
Time is gone, space is insane. Here it comes, here again.
sometimes it's okay to just guhhhhhhhhhhhhhh
Welcome to nginx!

View file

@ -0,0 +1,5 @@
# MaskedLinkPaste
Pasting a link while you have text selected will paste your link as a masked link at that location
![](https://github.com/Vendicated/Vencord/assets/78964224/1d3be2c6-7957-44c9-92ec-551069d46c02)

View file

@ -0,0 +1,36 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants.js";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
const linkRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
const { SlateTransforms } = findByPropsLazy("SlateTransforms");
export default definePlugin({
name: "MaskedLinkPaste",
authors: [Devs.TheSun],
description: "Pasting a link while having text selected will paste a hyperlink",
patches: [{
find: ".selection,preventEmojiSurrogates:",
replacement: {
match: /(?<=SlateTransforms.delete.{0,50})(\i)\.insertText\((\i)\)/,
replace: "$self.handlePaste($1, $2, () => $&)"
}
}],
handlePaste(editor, content: string, originalBehavior: () => void) {
if (content && linkRegex.test(content) && editor.operations?.[0]?.type === "remove_text") {
SlateTransforms.insertText(
editor,
`[${editor.operations[0].text}](${content})`
);
}
else originalBehavior();
}
});

View file

@ -70,8 +70,8 @@ export default definePlugin({
{ {
find: ".invitesDisabledTooltip", find: ".invitesDisabledTooltip",
replacement: { replacement: {
match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/, match: /\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100}(?=])/,
replace: ",$self.renderTooltip(arguments[0].guild)]" replace: "$&,$self.renderTooltip(arguments[0].guild)"
}, },
predicate: () => settings.store.toolTip predicate: () => settings.store.toolTip
} }

View file

@ -97,6 +97,9 @@ export default definePlugin({
// Message wasn't received through gateway // Message wasn't received through gateway
if (!isNonNullish(nonce)) return null; if (!isNonNullish(nonce)) return null;
// Bots basically never send a nonce, and if someone does do it then it's usually not a snowflake
if (message.bot) return null;
let isDiscordKotlin = false; let isDiscordKotlin = false;
let delta = SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce); // milliseconds let delta = SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce); // milliseconds
if (!showMillis) { if (!showMillis) {

View file

@ -376,6 +376,9 @@ export default definePlugin({
if (!messageLinkRegex.test(props.message.content)) if (!messageLinkRegex.test(props.message.content))
return null; return null;
// need to reset the regex because it's global
messageLinkRegex.lastIndex = 0;
return ( return (
<ErrorBoundary> <ErrorBoundary>
<MessageEmbedAccessory <MessageEmbedAccessory

View file

@ -3,6 +3,11 @@
color: var(--status-danger, #f04747) !important; color: var(--status-danger, #f04747) !important;
} }
/* Markdown title highlighting */
.messagelogger-deleted [class*="contents"] :is(h1, h2, h3) {
color: var(--status-danger, #f04747) !important;
}
/* Bot "thinking" text highlighting */ /* Bot "thinking" text highlighting */
.messagelogger-deleted [class*="colorStandard"] { .messagelogger-deleted [class*="colorStandard"] {
color: var(--status-danger, #f04747) !important; color: var(--status-danger, #f04747) !important;

View file

@ -295,12 +295,9 @@ export default definePlugin({
// }, // },
{ {
// Pass through editHistory & deleted & original attachments to the "edited message" transformer // Pass through editHistory & deleted & original attachments to the "edited message" transformer
match: /interactionData:(\i)\.interactionData/, match: /(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/,
replace: replace:
"interactionData:$1.interactionData," + "Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, attachments:$1.attachments })"
"deleted:$1.deleted," +
"editHistory:$1.editHistory," +
"attachments:$1.attachments"
}, },
// { // {

View file

@ -8,7 +8,7 @@
.emoji, .emoji,
[data-type="sticker"], [data-type="sticker"],
iframe, iframe,
.messagelogger-deleted-attachment, .messagelogger-deleted-attachment:not([class*="hiddenAttachment_"]),
[class|="inlineMediaEmbed"] [class|="inlineMediaEmbed"]
) { ) {
filter: grayscale(1) !important; filter: grayscale(1) !important;

View file

@ -111,8 +111,8 @@ export default definePlugin({
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)" replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
}, },
{ {
match: /(?<=scrollToChannel\(\i\){.{1,300})this\.props\.privateChannelIds/, match: /(scrollToChannel\(\i\){.{1,300})(this\.props\.privateChannelIds)/,
replace: "[...$&,...$self.getAllUncollapsedChannels()]" replace: "$1[...$2,...$self.getAllUncollapsedChannels()]"
}, },
] ]

View file

@ -24,6 +24,7 @@ import { ChannelStore, FluxDispatcher as Dispatcher, MessageStore, PermissionsBi
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
const Kangaroo = findByPropsLazy("jumpToMessage"); const Kangaroo = findByPropsLazy("jumpToMessage");
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
const isMac = navigator.platform.includes("Mac"); // bruh const isMac = navigator.platform.includes("Mac"); // bruh
let replyIdx = -1; let replyIdx = -1;
@ -139,6 +140,10 @@ function getNextMessage(isUp: boolean, isReply: boolean) {
messages = messages.filter(m => m.author.id === meId); messages = messages.filter(m => m.author.id === meId);
} }
if (Vencord.Plugins.isPluginEnabled("NoBlockedMessages")) {
messages = messages.filter(m => !RelationshipStore.isBlocked(m.author.id));
}
const mutate = (i: number) => isUp const mutate = (i: number) => isUp
? Math.min(messages.length - 1, i + 1) ? Math.min(messages.length - 1, i + 1)
: Math.max(-1, i - 1); : Math.max(-1, i - 1);

View file

@ -37,7 +37,7 @@ const settings = definePluginSettings({
}); });
function search(src: string, engine: string) { function search(src: string, engine: string) {
open(engine + encodeURIComponent(src), "_blank"); open(engine + encodeURIComponent(src.trim()), "_blank");
} }
function makeSearchItem(src: string) { function makeSearchItem(src: string) {

View file

@ -134,8 +134,8 @@ export default definePlugin({
{ {
find: '"MessageActionCreators"', find: '"MessageActionCreators"',
replacement: { replacement: {
match: /(?<=focusMessage\(\i\){.+?)(?=focus:{messageId:(\i)})/, match: /focusMessage\(\i\){.+?(?=focus:{messageId:(\i)})/,
replace: "after:$1," replace: "$&after:$1,"
} }
}, },
// Force Server Home instead of Server Guide // Force Server Home instead of Server Guide

View file

@ -0,0 +1,9 @@
# Summaries
Enables Discord's experimental Summaries feature on every server, displaying AI generated summaries of conversations.
Read more about summaries in the [official Discord help article](https://support.discord.com/hc/en-us/articles/12926016807575-In-Channel-Conversation-Summaries)!
Note that this plugin can't fetch old summaries, it can only display ones created while your Discord is running with the plugin enabled.
![](https://github.com/Vendicated/Vencord/assets/45497981/bd931b0c-2e85-4c10-9f7c-8ba01eb55745)

View file

@ -0,0 +1,115 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, GuildStore } from "@webpack/common";
const SummaryStore = findByPropsLazy("allSummaries", "findSummary");
const { createSummaryFromServer } = findByPropsLazy("createSummaryFromServer");
const settings = definePluginSettings({
summaryExpiryThresholdDays: {
type: OptionType.SLIDER,
description: "The time in days before a summary is removed. Note that only up to 50 summaries are kept per channel",
markers: [1, 3, 5, 7, 10, 15, 20, 25, 30],
stickToMarkers: false,
default: 3,
}
});
interface Summary {
count: number;
end_id: string;
id: string;
message_ids: string[];
people: string[];
source: number;
start_id: string;
summ_short: string;
topic: string;
type: number;
unsafe: boolean;
}
interface ChannelSummaries {
type: string;
channel_id: string;
guild_id: string;
summaries: Summary[];
// custom property
time?: number;
}
export default definePlugin({
name: "Summaries",
description: "Enables Discord's experimental Summaries feature on every server, displaying AI generated summaries of conversations",
authors: [Devs.mantikafasi],
settings,
patches: [
{
find: "ChannelTypesSets.SUMMARIZEABLE.has",
replacement: {
match: /\i\.hasFeature\(\i\.GuildFeatures\.SUMMARIES_ENABLED\w+?\)/g,
replace: "true"
}
},
{
find: "RECEIVE_CHANNEL_SUMMARY(",
replacement: {
match: /shouldFetch\((\i),\i\){/,
replace: "$& if(!$self.shouldFetch($1)) return false;"
}
}
],
flux: {
CONVERSATION_SUMMARY_UPDATE(data) {
const incomingSummaries: ChannelSummaries[] = data.summaries.map((summary: any) => ({ ...createSummaryFromServer(summary), time: Date.now() }));
// idk if this is good for performance but it doesnt seem to be a problem in my experience
DataStore.update("summaries-data", summaries => {
summaries ??= {};
summaries[data.channel_id] ? summaries[data.channel_id].unshift(...incomingSummaries) : (summaries[data.channel_id] = incomingSummaries);
if (summaries[data.channel_id].length > 50)
summaries[data.channel_id] = summaries[data.channel_id].slice(0, 50);
return summaries;
});
}
},
async start() {
await DataStore.update("summaries-data", summaries => {
summaries ??= {};
for (const key of Object.keys(summaries)) {
for (let i = summaries[key].length - 1; i >= 0; i--) {
if (summaries[key][i].time < Date.now() - 1000 * 60 * 60 * 24 * settings.store.summaryExpiryThresholdDays) {
summaries[key].splice(i, 1);
}
}
if (summaries[key].length === 0) {
delete summaries[key];
}
}
Object.assign(SummaryStore.allSummaries(), summaries);
return summaries;
});
},
shouldFetch(channelId: string) {
const channel = ChannelStore.getChannel(channelId);
// SUMMARIES_ENABLED feature is not in discord-types
const guild = GuildStore.getGuild(channel.guild_id);
// @ts-ignore
return guild.hasFeature("SUMMARIES_ENABLED_GA");
}
});

View file

@ -21,8 +21,7 @@ import "./shiki.css";
import { enableStyle } from "@api/Styles"; import { enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import previewExampleText from "file://previewExample.tsx";
import previewExampleText from "~fileContent/previewExample.tsx";
import { shiki } from "./api/shiki"; import { shiki } from "./api/shiki";
import { createHighlighter } from "./components/Highlighter"; import { createHighlighter } from "./components/Highlighter";

View file

@ -89,8 +89,8 @@ export default definePlugin({
}, },
// Remove permission checking for getRenderLevel function // Remove permission checking for getRenderLevel function
{ {
match: /(?<=getRenderLevel\(\i\){.+?return)!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,this\.record\)\|\|/, match: /(getRenderLevel\(\i\){.+?return)!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,this\.record\)\|\|/,
replace: " " replace: (_, rest) => `${rest} `
} }
] ]
}, },
@ -159,8 +159,8 @@ export default definePlugin({
replacement: [ replacement: [
// Make the channel appear as muted if it's hidden // Make the channel appear as muted if it's hidden
{ {
match: /(?<={channel:(\i),name:\i,muted:(\i).+?;)/, match: /{channel:(\i),name:\i,muted:(\i).+?;/,
replace: (_, channel, muted) => `${muted}=$self.isHiddenChannel(${channel})?true:${muted};` replace: (m, channel, muted) => `${m}${muted}=$self.isHiddenChannel(${channel})?true:${muted};`
}, },
// Add the hidden eye icon if the channel is hidden // Add the hidden eye icon if the channel is hidden
{ {
@ -186,8 +186,8 @@ export default definePlugin({
{ {
// Hide unreads // Hide unreads
predicate: () => settings.store.hideUnreads === true, predicate: () => settings.store.hideUnreads === true,
match: /(?<={channel:(\i),name:\i,.+?unread:(\i).+?;)/, match: /{channel:(\i),name:\i,.+?unread:(\i).+?;/,
replace: (_, channel, unread) => `${unread}=$self.isHiddenChannel(${channel})?false:${unread};` replace: (m, channel, unread) => `${m}${unread}=$self.isHiddenChannel(${channel})?false:${unread};`
} }
] ]
}, },

View file

@ -80,11 +80,19 @@ export default definePlugin({
} }
}, },
{ {
find: "auto_removed:", find: "prod_discoverable_guilds",
predicate: () => settings.store.disableDiscoveryFilters, predicate: () => settings.store.disableDiscoveryFilters,
replacement: { replacement: {
match: /filters:\i\.join\(" AND "\),facets:\[/, match: /\{"auto_removed:.*?\}/,
replace: "facets:[" replace: "{}"
}
},
{
find: "MINIMUM_MEMBER_COUNT:",
predicate: () => settings.store.disableDiscoveryFilters,
replacement: {
match: /MINIMUM_MEMBER_COUNT:function\(\)\{return \i}/,
replace: "MINIMUM_MEMBER_COUNT:() => \">0\""
} }
}, },
{ {

View file

@ -60,8 +60,8 @@ export default definePlugin({
}, },
{ {
predicate: () => settings.store.keepSpotifyActivityOnIdle, predicate: () => settings.store.keepSpotifyActivityOnIdle,
match: /(?<=shouldShowActivity\(\){.{0,50})&&!\i\.\i\.isIdle\(\)/, match: /(shouldShowActivity\(\){.{0,50})&&!\i\.\i\.isIdle\(\)/,
replace: "" replace: "$1"
} }
] ]
} }

View file

@ -42,8 +42,8 @@ export default definePlugin({
{ {
find: ",BURST_REACTION_EFFECT_PLAY", find: ",BURST_REACTION_EFFECT_PLAY",
replacement: { replacement: {
match: /(?<=BURST_REACTION_EFFECT_PLAY:\i=>{.{50,100})(\i\(\i,\i\))>=\d+/, match: /(BURST_REACTION_EFFECT_PLAY:\i=>{.{50,100})(\i\(\i,\i\))>=\d+/,
replace: "!$self.shouldPlayBurstReaction($1)" replace: "$1!$self.shouldPlayBurstReaction($2)"
} }
}, },
{ {

View file

@ -27,10 +27,10 @@ import style from "./index.css?managed";
const API_URL = "https://usrbg.is-hardly.online/users"; const API_URL = "https://usrbg.is-hardly.online/users";
interface UsrbgApiReturn { interface UsrbgApiReturn {
endpoint: string endpoint: string;
bucket: string bucket: string;
prefix: string prefix: string;
users: Record<string, string> users: Record<string, string>;
} }
const settings = definePluginSettings({ const settings = definePluginSettings({
@ -73,6 +73,19 @@ export default definePlugin({
} }
] ]
}, },
{
find: /overrideBannerSrc:\i,profileType:/,
replacement: [
{
match: /(\i)\.premiumType/,
replace: "$self.premiumHook($1)||$&"
},
{
match: /(?<=function \i\((\i)\)\{)(?=var.{30,50},overrideBannerSrc:)/,
replace: "$1.overrideBannerSrc=$self.useBannerHook($1);"
}
]
},
{ {
find: "\"data-selenium-video-tile\":", find: "\"data-selenium-video-tile\":",
predicate: () => settings.store.voiceBackground, predicate: () => settings.store.voiceBackground,

View file

@ -206,8 +206,8 @@ export default definePlugin({
{ {
find: ".avatarPositionPanel", find: ".avatarPositionPanel",
replacement: { replacement: {
match: /(?<=avatarWrapperNonUserBot.{0,50})onClick:(\i\|\|\i)\?void 0(?<=,avatarSrc:(\i).+?)/, match: /(avatarWrapperNonUserBot.{0,50})onClick:(\i\|\|\i)\?void 0(?<=,avatarSrc:(\i).+?)/,
replace: "style:($1)?{cursor:\"pointer\"}:{},onClick:$1?()=>{$self.openImage($2)}" replace: "$1style:($2)?{cursor:\"pointer\"}:{},onClick:$2?()=>{$self.openImage($3)}"
} }
}, },
// Group DMs top small & large icon // Group DMs top small & large icon

View file

@ -0,0 +1,6 @@
# WatchTogetherAdblock
Block ads in the YouTube WatchTogether activity via AdGuard
Note that this only works for yourself, other users in the activity will still see ads.
Powered by a modified version of [Adguard's BlockYoutubeAdsShortcut](https://github.com/AdguardTeam/BlockYouTubeAdsShortcut)

View file

@ -0,0 +1,262 @@
/* eslint-disable */
/**
* This file is part of AdGuard's Block YouTube Ads (https://github.com/AdguardTeam/BlockYouTubeAdsShortcut).
*
* Copyright (C) AdGuard Team
*
* AdGuard's Block YouTube Ads 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.
*
* AdGuard's Block YouTube Ads 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 AdGuard's Block YouTube Ads. If not, see <http://www.gnu.org/licenses/>.
*/
const LOGO_ID = "block-youtube-ads-logo";
const hiddenCSS = [
"#__ffYoutube1",
"#__ffYoutube2",
"#__ffYoutube3",
"#__ffYoutube4",
"#feed-pyv-container",
"#feedmodule-PRO",
"#homepage-chrome-side-promo",
"#merch-shelf",
"#offer-module",
'#pla-shelf > ytd-pla-shelf-renderer[class="style-scope ytd-watch"]',
"#pla-shelf",
"#premium-yva",
"#promo-info",
"#promo-list",
"#promotion-shelf",
"#related > ytd-watch-next-secondary-results-renderer > #items > ytd-compact-promoted-video-renderer.ytd-watch-next-secondary-results-renderer",
"#search-pva",
"#shelf-pyv-container",
"#video-masthead",
"#watch-branded-actions",
"#watch-buy-urls",
"#watch-channel-brand-div",
"#watch7-branded-banner",
"#YtKevlarVisibilityIdentifier",
"#YtSparklesVisibilityIdentifier",
".carousel-offer-url-container",
".companion-ad-container",
".GoogleActiveViewElement",
'.list-view[style="margin: 7px 0pt;"]',
".promoted-sparkles-text-search-root-container",
".promoted-videos",
".searchView.list-view",
".sparkles-light-cta",
".watch-extra-info-column",
".watch-extra-info-right",
".ytd-carousel-ad-renderer",
".ytd-compact-promoted-video-renderer",
".ytd-companion-slot-renderer",
".ytd-merch-shelf-renderer",
".ytd-player-legacy-desktop-watch-ads-renderer",
".ytd-promoted-sparkles-text-search-renderer",
".ytd-promoted-video-renderer",
".ytd-search-pyv-renderer",
".ytd-video-masthead-ad-v3-renderer",
".ytp-ad-action-interstitial-background-container",
".ytp-ad-action-interstitial-slot",
".ytp-ad-image-overlay",
".ytp-ad-overlay-container",
".ytp-ad-progress",
".ytp-ad-progress-list",
'[class*="ytd-display-ad-"]',
'[layout*="display-ad-"]',
'a[href^="http://www.youtube.com/cthru?"]',
'a[href^="https://www.youtube.com/cthru?"]',
"ytd-action-companion-ad-renderer",
"ytd-banner-promo-renderer",
"ytd-compact-promoted-video-renderer",
"ytd-companion-slot-renderer",
"ytd-display-ad-renderer",
"ytd-promoted-sparkles-text-search-renderer",
"ytd-promoted-sparkles-web-renderer",
"ytd-search-pyv-renderer",
"ytd-single-option-survey-renderer",
"ytd-video-masthead-ad-advertiser-info-renderer",
"ytd-video-masthead-ad-v3-renderer",
"YTM-PROMOTED-VIDEO-RENDERER",
];
/**
* Adds CSS to the page
*/
const hideElements = () => {
const selectors = hiddenCSS;
if (!selectors) {
return;
}
const rule = selectors.join(", ") + " { display: none!important; }";
const style = document.createElement("style");
style.innerHTML = rule;
document.head.appendChild(style);
};
/**
* Calls the "callback" function on every DOM change, but not for the tracked events
* @param {Function} callback callback function
*/
const observeDomChanges = callback => {
const domMutationObserver = new MutationObserver(mutations => {
callback(mutations);
});
domMutationObserver.observe(document.documentElement, {
childList: true,
subtree: true,
});
};
/**
* This function is supposed to be called on every DOM change
*/
const hideDynamicAds = () => {
const elements = document.querySelectorAll("#contents > ytd-rich-item-renderer ytd-display-ad-renderer");
if (elements.length === 0) {
return;
}
elements.forEach(el => {
if (el.parentNode && el.parentNode.parentNode) {
const parent = el.parentNode.parentNode;
if (parent.localName === "ytd-rich-item-renderer") {
parent.style.display = "none";
}
}
});
};
/**
* This function checks if the video ads are currently running
* and auto-clicks the skip button.
*/
const autoSkipAds = () => {
// If there's a video that plays the ad at this moment, scroll this ad
if (document.querySelector(".ad-showing")) {
const video = document.querySelector("video");
if (video && video.duration) {
video.currentTime = video.duration;
// Skip button should appear after that,
// now simply click it automatically
setTimeout(() => {
const skipBtn = document.querySelector("button.ytp-ad-skip-button");
if (skipBtn) {
skipBtn.click();
}
}, 100);
}
}
};
/**
* This function overrides a property on the specified object.
*
* @param {object} obj object to look for properties in
* @param {string} propertyName property to override
* @param {*} overrideValue value to set
*/
const overrideObject = (obj, propertyName, overrideValue) => {
if (!obj) {
return false;
}
let overriden = false;
for (const key in obj) {
// eslint-disable-next-line no-prototype-builtins
if (obj.hasOwnProperty(key) && key === propertyName) {
obj[key] = overrideValue;
overriden = true;
// eslint-disable-next-line no-prototype-builtins
} else if (obj.hasOwnProperty(key) && typeof obj[key] === "object") {
if (overrideObject(obj[key], propertyName, overrideValue)) {
overriden = true;
}
}
}
return overriden;
};
/**
* Overrides JSON.parse and Response.json functions.
* Examines these functions arguments, looks for properties with the specified name there
* and if it exists, changes it's value to what was specified.
*
* @param {string} propertyName name of the property
* @param {*} overrideValue new value for the property
*/
const jsonOverride = (propertyName, overrideValue) => {
const nativeJSONParse = JSON.parse;
JSON.parse = (...args) => {
const obj = nativeJSONParse.apply(this, args);
// Override it's props and return back to the caller
overrideObject(obj, propertyName, overrideValue);
return obj;
};
// Override Response.prototype.json
const nativeResponseJson = Response.prototype.json;
Response.prototype.json = new Proxy(nativeResponseJson, {
apply(...args) {
// Call the target function, get the original Promise
const promise = Reflect.apply(...args);
// Create a new one and override the JSON inside
return new Promise((resolve, reject) => {
promise.then(data => {
overrideObject(data, propertyName, overrideValue);
resolve(data);
}).catch(error => reject(error));
});
},
});
};
const addAdGuardLogoStyle = () => { };
const addAdGuardLogo = () => {
if (document.getElementById(LOGO_ID)) {
return;
}
const logo = document.createElement("span");
logo.innerHTML = "__logo_text__";
logo.setAttribute("id", LOGO_ID);
if (window.location.hostname === "m.youtube.com") {
const btn = document.querySelector("header.mobile-topbar-header > button");
if (btn) {
btn.parentNode?.insertBefore(logo, btn.nextSibling);
addAdGuardLogoStyle();
}
} else if (window.location.hostname === "www.youtube.com") {
const code = document.getElementById("country-code");
if (code) {
code.innerHTML = "";
code.appendChild(logo);
addAdGuardLogoStyle();
}
} else if (window.location.hostname === "music.youtube.com") {
const el = document.querySelector(".ytmusic-nav-bar#left-content");
if (el) {
el.appendChild(logo);
addAdGuardLogoStyle();
}
} else if (window.location.hostname === "www.youtube-nocookie.com") {
const code = document.querySelector("#yt-masthead #logo-container .content-region");
if (code) {
code.innerHTML = "";
code.appendChild(logo);
addAdGuardLogoStyle();
}
}
};
// Removes ads metadata from YouTube XHR requests
jsonOverride("adPlacements", []);
jsonOverride("playerAds", []);
// Applies CSS that hides YouTube ad elements
hideElements();
// Some changes should be re-evaluated on every page change
addAdGuardLogo();
hideDynamicAds();
autoSkipAds();
observeDomChanges(() => {
addAdGuardLogo();
hideDynamicAds();
autoSkipAds();
});

View file

@ -0,0 +1,15 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
// The entire code of this plugin can be found in native.ts
export default definePlugin({
name: "WatchTogetherAdblock",
description: "Block ads in the YouTube WatchTogether activity via AdGuard",
authors: [Devs.ImLvna],
});

View file

@ -0,0 +1,21 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { RendererSettings } from "@main/settings";
import { app } from "electron";
import adguard from "file://adguard.js?minify";
app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => {
if (frame.url.includes("discordsays") && frame.url.includes("youtube.com")) {
if (!RendererSettings.store.plugins?.WatchTogetherAdblock?.enabled) return;
frame.executeJavaScript(adguard);
}
});
});
});

View file

@ -186,7 +186,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
id: 296776625432035328n, id: 296776625432035328n,
}, },
TheSun: { TheSun: {
name: "ActuallyTheSun", name: "sunnie",
id: 406028027768733696n id: 406028027768733696n
}, },
axyie: { axyie: {
@ -402,6 +402,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "maisy", name: "maisy",
id: 257109471589957632n, id: 257109471589957632n,
}, },
Lexi: {
name: "Lexi",
id: 506101469787717658n
},
Mopi: { Mopi: {
name: "Mopi", name: "Mopi",
id: 1022189106614243350n id: 1022189106614243350n
@ -498,6 +502,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "ScattrdBlade", name: "ScattrdBlade",
id: 678007540608532491n id: 678007540608532491n
}, },
goodbee: {
name: "goodbee",
id: 658968552606400512n
},
Moxxie: { Moxxie: {
name: "Moxxie", name: "Moxxie",
id: 712653921692155965n, id: 712653921692155965n,

View file

@ -35,8 +35,8 @@ const unconfigurable = ["arguments", "caller", "prototype"];
const handler: ProxyHandler<any> = {}; const handler: ProxyHandler<any> = {};
const kGET = Symbol.for("vencord.lazy.get"); export const SYM_LAZY_GET = Symbol.for("vencord.lazy.get");
const kCACHE = Symbol.for("vencord.lazy.cached"); export const SYM_LAZY_CACHED = Symbol.for("vencord.lazy.cached");
for (const method of [ for (const method of [
"apply", "apply",
@ -53,11 +53,11 @@ for (const method of [
"setPrototypeOf" "setPrototypeOf"
]) { ]) {
handler[method] = handler[method] =
(target: any, ...args: any[]) => Reflect[method](target[kGET](), ...args); (target: any, ...args: any[]) => Reflect[method](target[SYM_LAZY_GET](), ...args);
} }
handler.ownKeys = target => { handler.ownKeys = target => {
const v = target[kGET](); const v = target[SYM_LAZY_GET]();
const keys = Reflect.ownKeys(v); const keys = Reflect.ownKeys(v);
for (const key of unconfigurable) { for (const key of unconfigurable) {
if (!keys.includes(key)) keys.push(key); if (!keys.includes(key)) keys.push(key);
@ -69,7 +69,7 @@ handler.getOwnPropertyDescriptor = (target, p) => {
if (typeof p === "string" && unconfigurable.includes(p)) if (typeof p === "string" && unconfigurable.includes(p))
return Reflect.getOwnPropertyDescriptor(target, p); return Reflect.getOwnPropertyDescriptor(target, p);
const descriptor = Reflect.getOwnPropertyDescriptor(target[kGET](), p); const descriptor = Reflect.getOwnPropertyDescriptor(target[SYM_LAZY_GET](), p);
if (descriptor) Object.defineProperty(target, p, descriptor); if (descriptor) Object.defineProperty(target, p, descriptor);
return descriptor; return descriptor;
@ -92,31 +92,34 @@ export function proxyLazy<T>(factory: () => T, attempts = 5, isChild = false): T
let tries = 0; let tries = 0;
const proxyDummy = Object.assign(function () { }, { const proxyDummy = Object.assign(function () { }, {
[kCACHE]: void 0 as T | undefined, [SYM_LAZY_CACHED]: void 0 as T | undefined,
[kGET]() { [SYM_LAZY_GET]() {
if (!proxyDummy[kCACHE] && attempts > tries++) { if (!proxyDummy[SYM_LAZY_CACHED] && attempts > tries++) {
proxyDummy[kCACHE] = factory(); proxyDummy[SYM_LAZY_CACHED] = factory();
if (!proxyDummy[kCACHE] && attempts === tries) if (!proxyDummy[SYM_LAZY_CACHED] && attempts === tries)
console.error("Lazy factory failed:", factory); console.error("Lazy factory failed:", factory);
} }
return proxyDummy[kCACHE]; return proxyDummy[SYM_LAZY_CACHED];
} }
}); });
return new Proxy(proxyDummy, { return new Proxy(proxyDummy, {
...handler, ...handler,
get(target, p, receiver) { get(target, p, receiver) {
if (p === SYM_LAZY_CACHED || p === SYM_LAZY_GET)
return Reflect.get(target, p, receiver);
// if we're still in the same tick, it means the lazy was immediately used. // if we're still in the same tick, it means the lazy was immediately used.
// thus, we lazy proxy the get access to make things like destructuring work as expected // thus, we lazy proxy the get access to make things like destructuring work as expected
// meow here will also be a lazy // meow here will also be a lazy
// `const { meow } = findByPropsLazy("meow");` // `const { meow } = findByPropsLazy("meow");`
if (!isChild && isSameTick) if (!isChild && isSameTick)
return proxyLazy( return proxyLazy(
() => Reflect.get(target[kGET](), p, receiver), () => Reflect.get(target[SYM_LAZY_GET](), p, receiver),
attempts, attempts,
true true
); );
const lazyTarget = target[kGET](); const lazyTarget = target[SYM_LAZY_GET]();
if (typeof lazyTarget === "object" || typeof lazyTarget === "function") { if (typeof lazyTarget === "object" || typeof lazyTarget === "function") {
return Reflect.get(lazyTarget, p, receiver); return Reflect.get(lazyTarget, p, receiver);
} }

File diff suppressed because one or more lines are too long

View file

@ -63,7 +63,7 @@ export interface CustomEmoji {
originalName?: string; originalName?: string;
require_colons: boolean; require_colons: boolean;
roles: string[]; roles: string[];
type: "GUILD_EMOJI"; type: 1;
} }
export interface UnicodeEmoji { export interface UnicodeEmoji {
@ -75,7 +75,7 @@ export interface UnicodeEmoji {
}; };
index: number; index: number;
surrogates: string; surrogates: string;
type: "UNICODE"; type: 0;
uniqueName: string; uniqueName: string;
useSpriteSheet: boolean; useSpriteSheet: boolean;
get allNamesString(): string; get allNamesString(): string;

View file

@ -99,6 +99,16 @@ Object.defineProperty(Function.prototype, "O", {
}; };
onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded); onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded);
// Returns whether a chunk has been loaded
Object.defineProperty(onChunksLoaded, "j", {
set(v) {
delete onChunksLoaded.j;
onChunksLoaded.j = v;
originalOnChunksLoaded.j = v;
},
configurable: true
});
} }
Object.defineProperty(this, "O", { Object.defineProperty(this, "O", {

View file

@ -402,7 +402,8 @@ export function findExportedComponentLazy<T extends object = any>(...props: stri
}); });
} }
const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\((\[\i\.\i\(".+?"\).+?\])\)|Promise\.resolve\(\)).then\(\i\.bind\(\i,"(.+?)"\)\)/; export const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\))|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/;
export const ChunkIdsRegex = /\("(.+?)"\)/g;
/** /**
* Extract and load chunks using their entry point * Extract and load chunks using their entry point
@ -431,7 +432,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
return; return;
} }
const [, rawChunkIds, entryPointId] = match; const [, rawChunkIdsArray, rawChunkIdsSingle, entryPointId] = match;
if (Number.isNaN(Number(entryPointId))) { if (Number.isNaN(Number(entryPointId))) {
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number"); const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number");
logger.warn(err, "Code:", code, "Matcher:", matcher); logger.warn(err, "Code:", code, "Matcher:", matcher);
@ -443,8 +444,9 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
return; return;
} }
const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
if (rawChunkIds) { if (rawChunkIds) {
const chunkIds = Array.from(rawChunkIds.matchAll(/\("(.+?)"\)/g)).map((m: any) => m[1]); const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]);
await Promise.all(chunkIds.map(id => wreq.e(id))); await Promise.all(chunkIds.map(id => wreq.e(id)));
} }