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,
"parser": "@typescript-eslint/parser",
"ignorePatterns": ["dist", "browser"],
"ignorePatterns": ["dist", "browser", "packages/vencord-types"],
"plugins": [
"@typescript-eslint",
"simple-header",

View file

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

View file

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

View file

@ -98,6 +98,9 @@ importers:
highlight.js:
specifier: 10.6.0
version: 10.6.0
html-minifier-terser:
specifier: ^7.2.0
version: 7.2.0
moment:
specifier: ^2.29.4
version: 2.29.4
@ -393,6 +396,27 @@ packages:
'@humanwhocodes/object-schema@1.2.1':
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':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -688,6 +712,9 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
camel-case@4.1.2:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
camelcase-keys@6.2.2:
resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
engines: {node: '>=8'}
@ -716,6 +743,10 @@ packages:
resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
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:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@ -740,6 +771,13 @@ packages:
colord@2.9.3:
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:
resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==}
@ -871,12 +909,19 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
end-of-stream@1.4.4:
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:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
@ -1402,6 +1447,11 @@ packages:
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
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:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'}
@ -1673,6 +1723,9 @@ packages:
lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
@ -1770,6 +1823,9 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
node-fetch@2.6.7:
resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
engines: {node: 4.x || >=6.0.0}
@ -1855,6 +1911,9 @@ packages:
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@ -1863,6 +1922,9 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
pascal-case@3.1.2:
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
pascalcase@0.1.1:
resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
engines: {node: '>=0.10.0'}
@ -1985,6 +2047,10 @@ packages:
resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==}
engines: {node: '>= 0.4'}
relateurl@0.2.7:
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
engines: {node: '>= 0.10'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@ -2228,6 +2294,11 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
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:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@ -2263,6 +2334,9 @@ packages:
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
tsutils@3.21.0:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
@ -2599,6 +2673,28 @@ snapshots:
'@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':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -2958,6 +3054,11 @@ snapshots:
callsites@3.1.0: {}
camel-case@4.1.2:
dependencies:
pascal-case: 3.1.2
tslib: 2.6.2
camelcase-keys@6.2.2:
dependencies:
camelcase: 5.3.1
@ -2991,6 +3092,10 @@ snapshots:
isobject: 3.0.1
static-extend: 0.1.2
clean-css@5.3.3:
dependencies:
source-map: 0.6.1
cliui@8.0.1:
dependencies:
string-width: 4.2.3
@ -3016,6 +3121,10 @@ snapshots:
colord@2.9.3: {}
commander@10.0.1: {}
commander@2.20.3: {}
component-emitter@1.3.0: {}
concat-map@0.0.1: {}
@ -3139,12 +3248,19 @@ snapshots:
dependencies:
esutils: 2.0.3
dot-case@3.0.4:
dependencies:
no-case: 3.0.4
tslib: 2.6.2
emoji-regex@8.0.0: {}
end-of-stream@1.4.4:
dependencies:
once: 1.4.0
entities@4.5.0: {}
error-ex@1.3.2:
dependencies:
is-arrayish: 0.2.1
@ -3733,6 +3849,16 @@ snapshots:
dependencies:
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: {}
https-proxy-agent@5.0.1:
@ -3974,6 +4100,10 @@ snapshots:
lodash.truncate@4.4.2: {}
lower-case@2.0.2:
dependencies:
tslib: 2.6.2
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
@ -4071,6 +4201,11 @@ snapshots:
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:
dependencies:
whatwg-url: 5.0.0
@ -4168,6 +4303,11 @@ snapshots:
pako@1.0.11: {}
param-case@3.0.4:
dependencies:
dot-case: 3.0.4
tslib: 2.6.2
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@ -4179,6 +4319,11 @@ snapshots:
json-parse-even-better-errors: 2.3.1
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: {}
path-exists@4.0.0: {}
@ -4296,6 +4441,8 @@ snapshots:
es-errors: 1.3.0
set-function-name: 2.0.2
relateurl@0.2.7: {}
require-directory@2.1.1: {}
require-from-string@2.0.2: {}
@ -4605,6 +4752,13 @@ snapshots:
inherits: 2.0.4
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: {}
through@2.3.8: {}
@ -4646,6 +4800,8 @@ snapshots:
tslib@1.14.1: {}
tslib@2.6.2: {}
tsutils@3.21.0(typescript@5.4.5):
dependencies:
tslib: 1.14.1

View file

@ -20,8 +20,10 @@ import "../suppressExperimentalWarnings.js";
import "../checkNodeVersion.js";
import { exec, execSync } from "child_process";
import esbuild from "esbuild";
import { constants as FsConstants, readFileSync } from "fs";
import { access, readdir, readFile } from "fs/promises";
import { minify as minifyHtml } from "html-minifier-terser";
import { join, relative } from "path";
import { promisify } from "util";
@ -161,21 +163,60 @@ export const gitRemotePlugin = {
/**
* @type {import("esbuild").Plugin}
*/
export const fileIncludePlugin = {
name: "file-include-plugin",
export const fileUrlPlugin = {
name: "file-uri-plugin",
setup: build => {
const filter = /^~fileContent\/.+$/;
const filter = /^file:\/\/.+$/;
build.onResolve({ filter }, args => ({
namespace: "include-file",
namespace: "file-uri",
path: args.path,
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 } }) => {
const [name, format] = path.split(";");
build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => {
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 {
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" : "",
legalComments: "linked",
banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement",

View file

@ -243,19 +243,27 @@ page.on("console", async e => {
}
}
if (isDebug) {
console.error(e.text());
} else if (level === "error") {
const text = await Promise.all(
e.args().map(async a => {
async function getText() {
try {
return await Promise.all(
e.args().map(async a => {
return await maybeGetError(a) || await a.jsonValue();
} catch (e) {
return a.toString();
}
})
).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")) {
console.error("[Unexpected Error]", text);
@ -322,22 +330,31 @@ async function runtime(token: string) {
const validChunks = new Set<string>();
const invalidChunks = new Set<string>();
const deferredRequires = new Set<string>();
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
// True if resolved, false otherwise
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) {
const lazyChunks = factoryCode.matchAll(lazyChunkRegex);
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const chunkIds = Array.from(rawChunkIds.matchAll(chunkIdsRegex)).map(m => m[1]);
if (chunkIds.length === 0) return;
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
// the chunk containing the component
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;
@ -373,6 +390,11 @@ async function runtime(token: string) {
// Requires the entry points for all valid chunk groups
for (const [, entryPoint] of validChunkGroups) {
try {
if (shouldForceDefer) {
deferredRequires.add(entryPoint);
continue;
}
if (wreq.m[entryPoint]) wreq(entryPoint as any);
} catch (err) {
console.error(err);
@ -435,6 +457,11 @@ async function runtime(token: string) {
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
const allChunks = [] as string[];
@ -514,7 +541,6 @@ async function runtime(token: string) {
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
} catch (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 { IpcEvents } from "@shared/IpcEvents";
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
import monacoHtml from "file://monacoWin.html?minify&base64";
import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
import { open, readdir, readFile } from "fs/promises";
import { join, normalize } from "path";
import monacoHtml from "~fileContent/monacoWin.html;base64";
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
import { makeLinksOpenExternally } from "./utils/externalLinks";

View file

@ -73,6 +73,9 @@ if (!IS_VANILLA) {
const original = options.webPreferences.preload;
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
options.webPreferences.sandbox = false;
// work around discord unloading when in background
options.webPreferences.backgroundThrottling = false;
if (settings.frameless) {
options.frame = false;
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
@ -136,6 +139,15 @@ if (!IS_VANILLA) {
}
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 {
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;
}
declare module "~fileContent/*" {
declare module "file://*" {
const content: string;
export default content;
}

View file

@ -29,7 +29,7 @@ export default definePlugin({
find: '"NoticeStore"',
replacement: [
{
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
match: /(?<=!1;)\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
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",
replacement: {

View file

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

View file

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

View file

@ -112,8 +112,8 @@ export default definePlugin({
replacement: [
// Create the isBetterFolders variable in the GuildsBar component
{
match: /(?<=let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?)(?=}=\i,)/,
replace: ",isBetterFolders"
match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i,)/,
replace: "$&,isBetterFolders"
},
// 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
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
*/
import { definePluginSettings } from "@api/Settings";
import { ImageIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { getCurrentGuild } from "@utils/discord";
import definePlugin from "@utils/types";
import { getCurrentGuild, openImageModal } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
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({
name: "BetterRoleContext",
description: "Adds options to copy role color / edit role when right clicking roles in the user profile",
authors: [Devs.Ven],
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
authors: [Devs.Ven, Devs.goodbee],
settings,
start() {
// 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) {
children.push(
<Menu.MenuItem

View file

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

View file

@ -34,9 +34,9 @@ export default definePlugin({
{
find: ".AVATAR_STATUS_MOBILE_16;",
replacement: {
match: /(?<=fromIsMobile:\i=!0,.+?)status:(\i)/,
match: /(fromIsMobile:\i=!0,.+?)status:(\i)/,
// 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 { getCurrentChannel, getCurrentGuild } from "@utils/discord";
import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy";
import { relaunch } from "@utils/native";
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 { extract, filters, findAll, search } from "@webpack";
import { React, ReactDOM } from "@webpack/common";
import { extract, filters, findAll, findModuleId, search } from "@webpack";
import * as Common from "@webpack/common";
import type { ComponentType } from "react";
const WEB_ONLY = (f: string) => () => {
const DESKTOP_ONLY = (f: string) => () => {
throw new Error(`'${f}' is Discord Desktop only.`);
};
export default definePlugin({
name: "ConsoleShortcuts",
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.",
authors: [Devs.Ven],
const define: typeof Object.defineProperty =
(obj, prop, desc) => {
if (Object.hasOwn(desc, "value"))
desc.writable = true;
getShortcuts() {
return Object.defineProperty(obj, prop, {
configurable: true,
enumerable: true,
...desc
});
};
function makeShortcuts() {
function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) {
const cache = new Map<string, unknown>();
@ -64,16 +73,17 @@ export default definePlugin({
let fakeRenderWin: WeakRef<Window> | undefined;
const find = newFindWrapper(f => f);
const findByProps = newFindWrapper(filters.byProps);
return {
...Vencord.Webpack.Common,
wp: Vencord.Webpack,
wpc: Webpack.wreq.c,
wreq: Webpack.wreq,
...Object.fromEntries(Object.keys(Common).map(key => [key, { getter: () => Common[key] }])),
wp: Webpack,
wpc: { getter: () => Webpack.cache },
wreq: { getter: () => Webpack.wreq },
wpsearch: search,
wpex: extract,
wpexs: (code: string) => extract(Webpack.findModuleId(code)!),
wpexs: (code: string) => extract(findModuleId(code)!),
find,
findAll,
findAll: findAll,
findByProps,
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
findByCode: newFindWrapper(filters.byCode),
@ -82,18 +92,21 @@ export default definePlugin({
findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)),
findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]],
findStore: newFindWrapper(filters.byStoreName),
PluginsApi: Vencord.Plugins,
plugins: Vencord.Plugins.plugins,
Settings: Vencord.Settings,
Api: Vencord.Api,
PluginsApi: { getter: () => Vencord.Plugins },
plugins: { getter: () => Vencord.Plugins.plugins },
Settings: { getter: () => Vencord.Settings },
Api: { getter: () => Vencord.Api },
Util: { getter: () => Vencord.Util },
reload: () => location.reload(),
restart: IS_WEB ? WEB_ONLY("restart") : relaunch,
restart: IS_WEB ? DESKTOP_ONLY("restart") : relaunch,
canonicalizeMatch,
canonicalizeReplace,
canonicalizeReplacement,
fakeRender: (component: ComponentType, props: any) => {
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);
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() {
const shortcuts = this.getShortcuts();
window.shortcutList = shortcuts;
for (const [key, val] of Object.entries(shortcuts))
const shortcuts = makeShortcuts();
window.shortcutList = {};
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;
}
}
// 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() {
delete window.shortcutList;
for (const key in this.getShortcuts())
for (const key in makeShortcuts()) {
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,
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"
},
{
@ -54,7 +54,7 @@ export default definePlugin({
result = event.shiftKey;
break;
case "ctrl+enter":
result = event.ctrlKey;
result = navigator.platform.includes("Mac") ? event.metaKey : event.ctrlKey;
break;
case "enter":
result = !event.shiftKey && !event.ctrlKey;

View file

@ -182,8 +182,8 @@ export default definePlugin({
// add dearrow button
{
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
replace: "children:[$self.renderButton(this),",
match: /children:\[(?=null!=\i\?(\i)\.renderSuppressButton)/,
replace: "children:[$self.renderButton($1),",
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
predicate: () => settings.store.transformStickers,
match: /(?<=renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;)/,
replace: (_, message, stickers) => `${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`
match: /renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;/,
replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`
},
{
// Filter attachments to remove fake nitro stickers or emojis
@ -813,7 +813,7 @@ export default definePlugin({
},
canUseEmote(e: Emoji, channelId: string) {
if (e.type === "UNICODE") return true;
if (e.type === 0) return true;
if (e.available === false) return false;
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
import "./index.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
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 { 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 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({
name: "FakeProfileThemes",
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.FormTitle tag="h3">Usage</Forms.FormTitle>
<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:
<ul>
<li> go to your profile settings</li>
<li> choose your own colors in the Nitro preview</li>
<li>
use the color pickers below to choose your colors
</li>
<li> click the "Copy 3y3" button</li>
<li> paste the invisible text anywhere in your bio</li>
</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.FormSection>),
</Forms.FormSection>);
},
settings,
colorDecodeHook(user: UserProfile) {
if (user) {

View file

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

View file

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

View file

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

View file

@ -18,88 +18,82 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
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.
// don't worry about it :P
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 presetQuotes = presetQuotesText.split("\n").map(quote => /^\s*[^#\s]/.test(quote) && quote.trim()).filter(Boolean) as string[];
const noQuotesQuote = "Did you really disable all loading quotes? What a buffoon you are...";
const settings = definePluginSettings({
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,
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({
name: "LoadingQuotes",
description: "Replace Discords loading quotes",
authors: [Devs.Ven, Devs.KraXen72],
authors: [Devs.Ven, Devs.KraXen72, Devs.UlyssesZhan],
settings,
patches: [
{
find: ".LOADING_DID_YOU_KNOW}",
find: ".LOADING_DID_YOU_KNOW",
replacement: [
{
match: /"_loadingText",function\(\)\{/,
replace: "$&return $self.quote;",
match: /"_loadingText".+?(?=(\i)\[.{0,10}\.random)/,
replace: "$&$self.mutateQuotes($1),"
},
{
match: /"_eventLoadingText",function\(\)\{/,
replace: "$&return $self.quote;",
match: /"_eventLoadingText".+?(?=(\i)\[.{0,10}\.random)/,
replace: "$&$self.mutateQuotes($1),",
predicate: () => settings.store.replaceEvents
}
],
]
},
],
xor(quote: string) {
const key = "read if cute";
const codes = Array.from(quote, (s, i) => s.charCodeAt(0) ^ (i % key.length));
return String.fromCharCode(...codes);
},
mutateQuotes(quotes: string[]) {
try {
const { enableDiscordPresetQuotes, additionalQuotes, additionalQuotesDelimiter, enablePluginPresetQuotes } = settings.store;
get quote() {
return this.xor(quotes[Math.floor(Math.random() * quotes.length)]);
if (!enableDiscordPresetQuotes)
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",
replacement: {
match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/,
replace: ",$self.renderTooltip(arguments[0].guild)]"
match: /\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100}(?=])/,
replace: "$&,$self.renderTooltip(arguments[0].guild)"
},
predicate: () => settings.store.toolTip
}

View file

@ -97,6 +97,9 @@ export default definePlugin({
// Message wasn't received through gateway
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 delta = SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce); // milliseconds
if (!showMillis) {

View file

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

View file

@ -3,6 +3,11 @@
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 */
.messagelogger-deleted [class*="colorStandard"] {
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
match: /interactionData:(\i)\.interactionData/,
match: /(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/,
replace:
"interactionData:$1.interactionData," +
"deleted:$1.deleted," +
"editHistory:$1.editHistory," +
"attachments:$1.attachments"
"Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, attachments:$1.attachments })"
},
// {

View file

@ -8,7 +8,7 @@
.emoji,
[data-type="sticker"],
iframe,
.messagelogger-deleted-attachment,
.messagelogger-deleted-attachment:not([class*="hiddenAttachment_"]),
[class|="inlineMediaEmbed"]
) {
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,$&)"
},
{
match: /(?<=scrollToChannel\(\i\){.{1,300})this\.props\.privateChannelIds/,
replace: "[...$&,...$self.getAllUncollapsedChannels()]"
match: /(scrollToChannel\(\i\){.{1,300})(this\.props\.privateChannelIds)/,
replace: "$1[...$2,...$self.getAllUncollapsedChannels()]"
},
]

View file

@ -24,6 +24,7 @@ import { ChannelStore, FluxDispatcher as Dispatcher, MessageStore, PermissionsBi
import { Message } from "discord-types/general";
const Kangaroo = findByPropsLazy("jumpToMessage");
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
const isMac = navigator.platform.includes("Mac"); // bruh
let replyIdx = -1;
@ -139,6 +140,10 @@ function getNextMessage(isUp: boolean, isReply: boolean) {
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
? Math.min(messages.length - 1, i + 1)
: Math.max(-1, i - 1);

View file

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

View file

@ -134,8 +134,8 @@ export default definePlugin({
{
find: '"MessageActionCreators"',
replacement: {
match: /(?<=focusMessage\(\i\){.+?)(?=focus:{messageId:(\i)})/,
replace: "after:$1,"
match: /focusMessage\(\i\){.+?(?=focus:{messageId:(\i)})/,
replace: "$&after:$1,"
}
},
// 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import previewExampleText from "~fileContent/previewExample.tsx";
import previewExampleText from "file://previewExample.tsx";
import { shiki } from "./api/shiki";
import { createHighlighter } from "./components/Highlighter";

View file

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

View file

@ -42,8 +42,8 @@ export default definePlugin({
{
find: ",BURST_REACTION_EFFECT_PLAY",
replacement: {
match: /(?<=BURST_REACTION_EFFECT_PLAY:\i=>{.{50,100})(\i\(\i,\i\))>=\d+/,
replace: "!$self.shouldPlayBurstReaction($1)"
match: /(BURST_REACTION_EFFECT_PLAY:\i=>{.{50,100})(\i\(\i,\i\))>=\d+/,
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";
interface UsrbgApiReturn {
endpoint: string
bucket: string
prefix: string
users: Record<string, string>
endpoint: string;
bucket: string;
prefix: string;
users: Record<string, string>;
}
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\":",
predicate: () => settings.store.voiceBackground,

View file

@ -206,8 +206,8 @@ export default definePlugin({
{
find: ".avatarPositionPanel",
replacement: {
match: /(?<=avatarWrapperNonUserBot.{0,50})onClick:(\i\|\|\i)\?void 0(?<=,avatarSrc:(\i).+?)/,
replace: "style:($1)?{cursor:\"pointer\"}:{},onClick:$1?()=>{$self.openImage($2)}"
match: /(avatarWrapperNonUserBot.{0,50})onClick:(\i\|\|\i)\?void 0(?<=,avatarSrc:(\i).+?)/,
replace: "$1style:($2)?{cursor:\"pointer\"}:{},onClick:$2?()=>{$self.openImage($3)}"
}
},
// 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,
},
TheSun: {
name: "ActuallyTheSun",
name: "sunnie",
id: 406028027768733696n
},
axyie: {
@ -402,6 +402,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "maisy",
id: 257109471589957632n,
},
Lexi: {
name: "Lexi",
id: 506101469787717658n
},
Mopi: {
name: "Mopi",
id: 1022189106614243350n
@ -498,6 +502,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "ScattrdBlade",
id: 678007540608532491n
},
goodbee: {
name: "goodbee",
id: 658968552606400512n
},
Moxxie: {
name: "Moxxie",
id: 712653921692155965n,

View file

@ -35,8 +35,8 @@ const unconfigurable = ["arguments", "caller", "prototype"];
const handler: ProxyHandler<any> = {};
const kGET = Symbol.for("vencord.lazy.get");
const kCACHE = Symbol.for("vencord.lazy.cached");
export const SYM_LAZY_GET = Symbol.for("vencord.lazy.get");
export const SYM_LAZY_CACHED = Symbol.for("vencord.lazy.cached");
for (const method of [
"apply",
@ -53,11 +53,11 @@ for (const method of [
"setPrototypeOf"
]) {
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 => {
const v = target[kGET]();
const v = target[SYM_LAZY_GET]();
const keys = Reflect.ownKeys(v);
for (const key of unconfigurable) {
if (!keys.includes(key)) keys.push(key);
@ -69,7 +69,7 @@ handler.getOwnPropertyDescriptor = (target, p) => {
if (typeof p === "string" && unconfigurable.includes(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);
return descriptor;
@ -92,31 +92,34 @@ export function proxyLazy<T>(factory: () => T, attempts = 5, isChild = false): T
let tries = 0;
const proxyDummy = Object.assign(function () { }, {
[kCACHE]: void 0 as T | undefined,
[kGET]() {
if (!proxyDummy[kCACHE] && attempts > tries++) {
proxyDummy[kCACHE] = factory();
if (!proxyDummy[kCACHE] && attempts === tries)
[SYM_LAZY_CACHED]: void 0 as T | undefined,
[SYM_LAZY_GET]() {
if (!proxyDummy[SYM_LAZY_CACHED] && attempts > tries++) {
proxyDummy[SYM_LAZY_CACHED] = factory();
if (!proxyDummy[SYM_LAZY_CACHED] && attempts === tries)
console.error("Lazy factory failed:", factory);
}
return proxyDummy[kCACHE];
return proxyDummy[SYM_LAZY_CACHED];
}
});
return new Proxy(proxyDummy, {
...handler,
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.
// thus, we lazy proxy the get access to make things like destructuring work as expected
// meow here will also be a lazy
// `const { meow } = findByPropsLazy("meow");`
if (!isChild && isSameTick)
return proxyLazy(
() => Reflect.get(target[kGET](), p, receiver),
() => Reflect.get(target[SYM_LAZY_GET](), p, receiver),
attempts,
true
);
const lazyTarget = target[kGET]();
const lazyTarget = target[SYM_LAZY_GET]();
if (typeof lazyTarget === "object" || typeof lazyTarget === "function") {
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;
require_colons: boolean;
roles: string[];
type: "GUILD_EMOJI";
type: 1;
}
export interface UnicodeEmoji {
@ -75,7 +75,7 @@ export interface UnicodeEmoji {
};
index: number;
surrogates: string;
type: "UNICODE";
type: 0;
uniqueName: string;
useSpriteSheet: boolean;
get allNamesString(): string;

View file

@ -99,6 +99,16 @@ Object.defineProperty(Function.prototype, "O", {
};
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", {

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
@ -431,7 +432,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
return;
}
const [, rawChunkIds, entryPointId] = match;
const [, rawChunkIdsArray, rawChunkIdsSingle, entryPointId] = match;
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");
logger.warn(err, "Code:", code, "Matcher:", matcher);
@ -443,8 +444,9 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
return;
}
const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
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)));
}