This commit is contained in:
Manti 2022-12-09 19:21:14 +03:00
commit f8b026dbae
62 changed files with 2473 additions and 465 deletions

View file

@ -1,4 +1,4 @@
name: Build latest name: Build DevBuild
on: on:
push: push:
branches: branches:
@ -9,6 +9,7 @@ on:
- browser/** - browser/**
- scripts/build/** - scripts/build/**
- package.json - package.json
- pnpm-lock.yaml
env: env:
FORCE_COLOR: true FORCE_COLOR: true
@ -21,7 +22,7 @@ jobs:
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18 - name: Use Node.js 19
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 19 node-version: 19
@ -35,7 +36,7 @@ jobs:
- name: Sign firefox extension - name: Sign firefox extension
run: | run: |
pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=listed pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=unlisted
env: env:
WEBEXT_USER: ${{ secrets.WEBEXT_USER }} WEBEXT_USER: ${{ secrets.WEBEXT_USER }}
WEBEXT_SECRET: ${{ secrets.WEBEXT_SECRET }} WEBEXT_SECRET: ${{ secrets.WEBEXT_SECRET }}
@ -47,7 +48,7 @@ jobs:
run: | run: |
mv dist/*.xpi dist/Vencord-for-Firefox.xpi mv dist/*.xpi dist/Vencord-for-Firefox.xpi
mv dist/extension-v3.zip dist/Vencord-for-Chrome-and-Edge.zip mv dist/extension-v3.zip dist/Vencord-for-Chrome-and-Edge.zip
rm -rf dist/extension-v2-unpacked rm -rf dist/extension-v2-unpacked dist/extension-v2.zip
- name: Get some values needed for the release - name: Get some values needed for the release
id: release_values id: release_values

3
.gitignore vendored
View file

@ -18,3 +18,6 @@ lerna-debug.log*
*.tsbuildinfo *.tsbuildinfo
src/userplugins src/userplugins
ExtensionCache/
settings/

View file

@ -19,7 +19,11 @@ If you're a power user who wants to contribute and make plugins or just want to
## Installing on Browser ## Installing on Browser
Install the browser extension for [![Chrome](https://img.shields.io/badge/chrome-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip), [![Firefox](https://img.shields.io/badge/firefox-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Firefox.xpi) or [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js). Please note that they aren't automatically updated for now, so you will regularely have to reinstall it. [![Get the Firefox extension](https://blog.mozilla.org/addons/files/2015/11/get-the-addon-small.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/)
Or install the browser extension for
- [![Chrome](https://img.shields.io/badge/chrome-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip)
- [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js) - Please note that QuickCSS, shiki and other plugins making use of external resources will not work with the UserScript.
You may also build them from source, to do that do the same steps as in the manual regular install method, You may also build them from source, to do that do the same steps as in the manual regular install method,

107
browser/GMPolyfill.js Normal file
View file

@ -0,0 +1,107 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function fetchOptions(url) {
return new Promise((resolve, reject) => {
const opt = {
method: "OPTIONS",
url: url,
};
opt.onload = resp => resolve(resp.responseHeaders);
opt.ontimeout = () => reject("fetch timeout");
opt.onerror = () => reject("fetch error");
opt.onabort = () => reject("fetch abort");
GM_xmlhttpRequest(opt);
});
}
function parseHeaders(headers) {
if (!headers)
return {};
const result = {};
const headersArr = headers.trim().split("\n");
for (var i = 0; i < headersArr.length; i++) {
var row = headersArr[i];
var index = row.indexOf(":")
, key = row.slice(0, index).trim().toLowerCase()
, value = row.slice(index + 1).trim();
if (result[key] === undefined) {
result[key] = value;
} else if (Array.isArray(result[key])) {
result[key].push(value);
} else {
result[key] = [result[key], value];
}
}
return result;
}
// returns true if CORS permits request
async function checkCors(url, method) {
const headers = parseHeaders(await fetchOptions(url));
const origin = headers["access-control-allow-origin"];
if (origin !== "*" && origin !== window.location.origin) return false;
const methods = headers["access-control-allow-methods"]?.split(/,\s/g);
if (methods && !methods.includes(method)) return false;
return true;
}
function blobTo(to, blob) {
if (to === "arrayBuffer" && blob.arrayBuffer) return blob.arrayBuffer();
return new Promise((resolve, reject) => {
var fileReader = new FileReader();
fileReader.onload = event => resolve(event.target.result);
if (to === "arrayBuffer") fileReader.readAsArrayBuffer(blob);
else if (to === "text") fileReader.readAsText(blob, "utf-8");
else reject("unknown to");
});
}
function GM_fetch(url, opt) {
return new Promise((resolve, reject) => {
checkCors(url, opt?.method || "GET")
.then(can => {
if (can) {
// https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
const options = opt || {};
options.url = url;
options.data = options.body;
options.responseType = "blob";
options.onload = resp => {
var blob = resp.response;
resp.blob = () => Promise.resolve(blob);
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
resp.text = () => blobTo("text", blob);
resp.json = async () => JSON.parse(await blobTo("text", blob));
resolve(resp);
};
options.ontimeout = () => reject("fetch timeout");
options.onerror = () => reject("fetch error");
options.onabort = () => reject("fetch abort");
GM_xmlhttpRequest(options);
} else {
reject("CORS issue");
}
});
});
}
export const fetch = GM_fetch;

View file

@ -21,11 +21,5 @@
"web_accessible_resources": ["dist/Vencord.js"], "web_accessible_resources": ["dist/Vencord.js"],
"background": { "background": {
"scripts": ["background.js"] "scripts": ["background.js"]
},
"browser_specific_settings": {
"gecko": {
"id": "vencord-firefox@vendicated.dev",
"strict_min_version": "92.0"
}
} }
} }

View file

@ -7,7 +7,7 @@
// @supportURL https://github.com/Vendicated/Vencord // @supportURL https://github.com/Vendicated/Vencord
// @license GPL-3.0 // @license GPL-3.0
// @match *://*.discord.com/* // @match *://*.discord.com/*
// @grant none // @grant GM_xmlhttpRequest
// @run-at document-start // @run-at document-start
// @compatible chrome Chrome + Tampermonkey or Violentmonkey // @compatible chrome Chrome + Tampermonkey or Violentmonkey
// @compatible firefox Firefox Tampermonkey // @compatible firefox Firefox Tampermonkey

View file

@ -40,6 +40,8 @@
"@types/yazl": "^2.4.2", "@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^5.44.0", "@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0", "@typescript-eslint/parser": "^5.44.0",
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.3",
"console-menu": "^0.1.0", "console-menu": "^0.1.0",
"diff": "^5.1.0", "diff": "^5.1.0",
"discord-types": "^1.3.26", "discord-types": "^1.3.26",
@ -50,6 +52,7 @@
"eslint-plugin-path-alias": "^1.0.0", "eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-simple-import-sort": "^8.0.0", "eslint-plugin-simple-import-sort": "^8.0.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"highlight.js": "10.6.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"puppeteer-core": "^19.3.0", "puppeteer-core": "^19.3.0",
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^1.0.0",

182
pnpm-lock.yaml generated
View file

@ -13,6 +13,8 @@ specifiers:
'@types/yazl': ^2.4.2 '@types/yazl': ^2.4.2
'@typescript-eslint/eslint-plugin': ^5.44.0 '@typescript-eslint/eslint-plugin': ^5.44.0
'@typescript-eslint/parser': ^5.44.0 '@typescript-eslint/parser': ^5.44.0
'@vap/core': 0.0.12
'@vap/shiki': 0.10.3
console-menu: ^0.1.0 console-menu: ^0.1.0
diff: ^5.1.0 diff: ^5.1.0
discord-types: ^1.3.26 discord-types: ^1.3.26
@ -24,6 +26,7 @@ specifiers:
eslint-plugin-simple-import-sort: ^8.0.0 eslint-plugin-simple-import-sort: ^8.0.0
eslint-plugin-unused-imports: ^2.0.0 eslint-plugin-unused-imports: ^2.0.0
fflate: ^0.7.4 fflate: ^0.7.4
highlight.js: 10.6.0
moment: ^2.29.4 moment: ^2.29.4
puppeteer-core: ^19.3.0 puppeteer-core: ^19.3.0
standalone-electron-types: ^1.0.0 standalone-electron-types: ^1.0.0
@ -39,8 +42,10 @@ devDependencies:
'@types/react': 18.0.25 '@types/react': 18.0.25
'@types/react-dom': 18.0.9 '@types/react-dom': 18.0.9
'@types/yazl': 2.4.2 '@types/yazl': 2.4.2
'@typescript-eslint/eslint-plugin': 5.44.0_fnsv2sbzcckq65bwfk7a5xwslu '@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au
'@typescript-eslint/parser': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
'@vap/core': 0.0.12
'@vap/shiki': 0.10.3
console-menu: 0.1.0 console-menu: 0.1.0
diff: 5.1.0 diff: 5.1.0
discord-types: 1.3.26 discord-types: 1.3.26
@ -50,7 +55,8 @@ devDependencies:
eslint-plugin-header: 3.1.1_eslint@8.28.0 eslint-plugin-header: 3.1.1_eslint@8.28.0
eslint-plugin-path-alias: 1.0.0_m6sma4g6bh67km3q6igf6uxaja_eslint@8.28.0 eslint-plugin-path-alias: 1.0.0_m6sma4g6bh67km3q6igf6uxaja_eslint@8.28.0
eslint-plugin-simple-import-sort: 8.0.0_eslint@8.28.0 eslint-plugin-simple-import-sort: 8.0.0_eslint@8.28.0
eslint-plugin-unused-imports: 2.0.0_aucl44mjeutxyzmt4nvo2cczya eslint-plugin-unused-imports: 2.0.0_5am2datodjm2qi4eijrjrnoz54
highlight.js: 10.6.0
moment: 2.29.4 moment: 2.29.4
puppeteer-core: 19.3.0 puppeteer-core: 19.3.0
standalone-electron-types: 1.0.0 standalone-electron-types: 1.0.0
@ -83,9 +89,9 @@ packages:
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
debug: 4.3.4 debug: 4.3.4
espree: 9.4.1 espree: 9.4.0
globals: 13.18.0 globals: 13.17.0
ignore: 5.2.1 ignore: 5.2.0
import-fresh: 3.3.0 import-fresh: 3.3.0
js-yaml: 4.1.0 js-yaml: 4.1.0
minimatch: 3.1.2 minimatch: 3.1.2
@ -161,7 +167,7 @@ packages:
resolution: {integrity: sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==} resolution: {integrity: sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==}
dependencies: dependencies:
'@types/prop-types': 15.7.5 '@types/prop-types': 15.7.5
csstype: 3.1.1 csstype: 3.1.0
dev: true dev: true
/@types/react/18.0.25: /@types/react/18.0.25:
@ -169,7 +175,7 @@ packages:
dependencies: dependencies:
'@types/prop-types': 15.7.5 '@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2 '@types/scheduler': 0.16.2
csstype: 3.1.1 csstype: 3.1.0
dev: true dev: true
/@types/scheduler/0.16.2: /@types/scheduler/0.16.2:
@ -194,8 +200,8 @@ packages:
'@types/node': 18.11.9 '@types/node': 18.11.9
dev: true dev: true
/@typescript-eslint/eslint-plugin/5.44.0_fnsv2sbzcckq65bwfk7a5xwslu: /@typescript-eslint/eslint-plugin/5.45.0_czs5uoqkd3podpy6vgtsxfc7au:
resolution: {integrity: sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw==} resolution: {integrity: sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
'@typescript-eslint/parser': ^5.0.0 '@typescript-eslint/parser': ^5.0.0
@ -205,24 +211,24 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/parser': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
'@typescript-eslint/scope-manager': 5.44.0 '@typescript-eslint/scope-manager': 5.45.0
'@typescript-eslint/type-utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a '@typescript-eslint/type-utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
'@typescript-eslint/utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
debug: 4.3.4 debug: 4.3.4
eslint: 8.28.0 eslint: 8.28.0
ignore: 5.2.1 ignore: 5.2.0
natural-compare-lite: 1.4.0 natural-compare-lite: 1.4.0
regexpp: 3.2.0 regexpp: 3.2.0
semver: 7.3.8 semver: 7.3.7
tsutils: 3.21.0_typescript@4.9.3 tsutils: 3.21.0_typescript@4.9.3
typescript: 4.9.3 typescript: 4.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
/@typescript-eslint/parser/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a: /@typescript-eslint/parser/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a:
resolution: {integrity: sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA==} resolution: {integrity: sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
@ -231,9 +237,9 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/scope-manager': 5.44.0 '@typescript-eslint/scope-manager': 5.45.0
'@typescript-eslint/types': 5.44.0 '@typescript-eslint/types': 5.45.0
'@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3 '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3
debug: 4.3.4 debug: 4.3.4
eslint: 8.28.0 eslint: 8.28.0
typescript: 4.9.3 typescript: 4.9.3
@ -241,16 +247,16 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@typescript-eslint/scope-manager/5.44.0: /@typescript-eslint/scope-manager/5.45.0:
resolution: {integrity: sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g==} resolution: {integrity: sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies: dependencies:
'@typescript-eslint/types': 5.44.0 '@typescript-eslint/types': 5.45.0
'@typescript-eslint/visitor-keys': 5.44.0 '@typescript-eslint/visitor-keys': 5.45.0
dev: true dev: true
/@typescript-eslint/type-utils/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a: /@typescript-eslint/type-utils/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a:
resolution: {integrity: sha512-A1u0Yo5wZxkXPQ7/noGkRhV4J9opcymcr31XQtOzcc5nO/IHN2E2TPMECKWYpM3e6olWEM63fq/BaL1wEYnt/w==} resolution: {integrity: sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
eslint: '*' eslint: '*'
@ -259,8 +265,8 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3 '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3
'@typescript-eslint/utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a
debug: 4.3.4 debug: 4.3.4
eslint: 8.28.0 eslint: 8.28.0
tsutils: 3.21.0_typescript@4.9.3 tsutils: 3.21.0_typescript@4.9.3
@ -269,13 +275,13 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@typescript-eslint/types/5.44.0: /@typescript-eslint/types/5.45.0:
resolution: {integrity: sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ==} resolution: {integrity: sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true dev: true
/@typescript-eslint/typescript-estree/5.44.0_typescript@4.9.3: /@typescript-eslint/typescript-estree/5.45.0_typescript@4.9.3:
resolution: {integrity: sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==} resolution: {integrity: sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
typescript: '*' typescript: '*'
@ -283,56 +289,70 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/types': 5.44.0 '@typescript-eslint/types': 5.45.0
'@typescript-eslint/visitor-keys': 5.44.0 '@typescript-eslint/visitor-keys': 5.45.0
debug: 4.3.4 debug: 4.3.4
globby: 11.1.0 globby: 11.1.0
is-glob: 4.0.3 is-glob: 4.0.3
semver: 7.3.8 semver: 7.3.7
tsutils: 3.21.0_typescript@4.9.3 tsutils: 3.21.0_typescript@4.9.3
typescript: 4.9.3 typescript: 4.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
/@typescript-eslint/utils/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a: /@typescript-eslint/utils/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a:
resolution: {integrity: sha512-fMzA8LLQ189gaBjS0MZszw5HBdZgVwxVFShCO3QN+ws3GlPkcy9YuS3U4wkT6su0w+Byjq3mS3uamy9HE4Yfjw==} resolution: {integrity: sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies: dependencies:
'@types/json-schema': 7.0.11 '@types/json-schema': 7.0.11
'@types/semver': 7.3.13 '@types/semver': 7.3.13
'@typescript-eslint/scope-manager': 5.44.0 '@typescript-eslint/scope-manager': 5.45.0
'@typescript-eslint/types': 5.44.0 '@typescript-eslint/types': 5.45.0
'@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3 '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3
eslint: 8.28.0 eslint: 8.28.0
eslint-scope: 5.1.1 eslint-scope: 5.1.1
eslint-utils: 3.0.0_eslint@8.28.0 eslint-utils: 3.0.0_eslint@8.28.0
semver: 7.3.8 semver: 7.3.7
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- typescript - typescript
dev: true dev: true
/@typescript-eslint/visitor-keys/5.44.0: /@typescript-eslint/visitor-keys/5.45.0:
resolution: {integrity: sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ==} resolution: {integrity: sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies: dependencies:
'@typescript-eslint/types': 5.44.0 '@typescript-eslint/types': 5.45.0
eslint-visitor-keys: 3.3.0 eslint-visitor-keys: 3.3.0
dev: true dev: true
/acorn-jsx/5.3.2_acorn@8.8.1: /@vap/core/0.0.12:
resolution: {integrity: sha512-3csHpkE1zUSRTZwl7xIf2uXg1cD4IhhtUm0F6K/dWydc95R5Nj+krB4OTNATuqkewIv/ViCbwjPfkafAgvZQSg==}
dependencies:
eventemitter3: 4.0.7
dev: true
/@vap/shiki/0.10.3:
resolution: {integrity: sha512-tZPHZxDKEBlorQ2BaprytGfkbo5yKBvdxdAF144p94HCTpjO3ScJk/f319wi7GtV1NE4DV8HBQo/0XpldixWQA==}
dependencies:
jsonc-parser: 3.2.0
vscode-oniguruma: 1.7.0
vscode-textmate: 5.2.0
dev: true
/acorn-jsx/5.3.2_acorn@8.8.0:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies: peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies: dependencies:
acorn: 8.8.1 acorn: 8.8.0
dev: true dev: true
/acorn/8.8.1: /acorn/8.8.0:
resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
dev: true dev: true
@ -522,7 +542,7 @@ packages:
dev: true dev: true
/concat-map/0.0.1: /concat-map/0.0.1:
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true dev: true
/console-menu/0.1.0: /console-menu/0.1.0:
@ -553,8 +573,8 @@ packages:
which: 2.0.2 which: 2.0.2
dev: true dev: true
/csstype/3.1.1: /csstype/3.1.0:
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==}
dev: true dev: true
/debug/2.6.9: /debug/2.6.9:
@ -897,7 +917,7 @@ packages:
eslint: 8.28.0 eslint: 8.28.0
dev: true dev: true
/eslint-plugin-unused-imports/2.0.0_aucl44mjeutxyzmt4nvo2cczya: /eslint-plugin-unused-imports/2.0.0_5am2datodjm2qi4eijrjrnoz54:
resolution: {integrity: sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==} resolution: {integrity: sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
@ -907,7 +927,7 @@ packages:
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/eslint-plugin': 5.44.0_fnsv2sbzcckq65bwfk7a5xwslu '@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au
eslint: 8.28.0 eslint: 8.28.0
eslint-rule-composer: 0.3.0 eslint-rule-composer: 0.3.0
dev: true dev: true
@ -971,21 +991,21 @@ packages:
eslint-scope: 7.1.1 eslint-scope: 7.1.1
eslint-utils: 3.0.0_eslint@8.28.0 eslint-utils: 3.0.0_eslint@8.28.0
eslint-visitor-keys: 3.3.0 eslint-visitor-keys: 3.3.0
espree: 9.4.1 espree: 9.4.0
esquery: 1.4.0 esquery: 1.4.0
esutils: 2.0.3 esutils: 2.0.3
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
file-entry-cache: 6.0.1 file-entry-cache: 6.0.1
find-up: 5.0.0 find-up: 5.0.0
glob-parent: 6.0.2 glob-parent: 6.0.2
globals: 13.18.0 globals: 13.17.0
grapheme-splitter: 1.0.4 grapheme-splitter: 1.0.4
ignore: 5.2.1 ignore: 5.2.0
import-fresh: 3.3.0 import-fresh: 3.3.0
imurmurhash: 0.1.4 imurmurhash: 0.1.4
is-glob: 4.0.3 is-glob: 4.0.3
is-path-inside: 3.0.3 is-path-inside: 3.0.3
js-sdsl: 4.2.0 js-sdsl: 4.1.5
js-yaml: 4.1.0 js-yaml: 4.1.0
json-stable-stringify-without-jsonify: 1.0.1 json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1 levn: 0.4.1
@ -1001,12 +1021,12 @@ packages:
- supports-color - supports-color
dev: true dev: true
/espree/9.4.1: /espree/9.4.0:
resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies: dependencies:
acorn: 8.8.1 acorn: 8.8.0
acorn-jsx: 5.3.2_acorn@8.8.1 acorn-jsx: 5.3.2_acorn@8.8.0
eslint-visitor-keys: 3.3.0 eslint-visitor-keys: 3.3.0
dev: true dev: true
@ -1039,6 +1059,10 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/eventemitter3/4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
dev: true
/extend-shallow/2.0.1: /extend-shallow/2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -1198,8 +1222,8 @@ packages:
path-is-absolute: 1.0.1 path-is-absolute: 1.0.1
dev: true dev: true
/globals/13.18.0: /globals/13.17.0:
resolution: {integrity: sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==} resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==}
engines: {node: '>=8'} engines: {node: '>=8'}
dependencies: dependencies:
type-fest: 0.20.2 type-fest: 0.20.2
@ -1212,7 +1236,7 @@ packages:
array-union: 2.1.0 array-union: 2.1.0
dir-glob: 3.0.1 dir-glob: 3.0.1
fast-glob: 3.2.12 fast-glob: 3.2.12
ignore: 5.2.1 ignore: 5.2.0
merge2: 1.4.1 merge2: 1.4.1
slash: 3.0.0 slash: 3.0.0
dev: true dev: true
@ -1257,6 +1281,10 @@ packages:
kind-of: 4.0.0 kind-of: 4.0.0
dev: true dev: true
/highlight.js/10.6.0:
resolution: {integrity: sha512-8mlRcn5vk/r4+QcqerapwBYTe+iPL5ih6xrNylxrnBdHQiijDETfXX7VIxC3UiCRiINBJfANBAsPzAvRQj8RpQ==}
dev: true
/https-proxy-agent/5.0.1: /https-proxy-agent/5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -1271,8 +1299,8 @@ packages:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: true dev: true
/ignore/5.2.1: /ignore/5.2.0:
resolution: {integrity: sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==} resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
dev: true dev: true
@ -1423,8 +1451,8 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/js-sdsl/4.2.0: /js-sdsl/4.1.5:
resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==}
dev: true dev: true
/js-yaml/4.1.0: /js-yaml/4.1.0:
@ -1442,6 +1470,10 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true dev: true
/jsonc-parser/3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
dev: true
/keypress/0.2.1: /keypress/0.2.1:
resolution: {integrity: sha512-HjorDJFNhnM4SicvaUXac0X77NiskggxJdesG72+O5zBKpSqKFCrqmndKVqpu3pFqkla0St6uGk8Ju0sCurrmg==} resolution: {integrity: sha512-HjorDJFNhnM4SicvaUXac0X77NiskggxJdesG72+O5zBKpSqKFCrqmndKVqpu3pFqkla0St6uGk8Ju0sCurrmg==}
dev: true dev: true
@ -1797,8 +1829,8 @@ packages:
ret: 0.1.15 ret: 0.1.15
dev: true dev: true
/semver/7.3.8: /semver/7.3.7:
resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==}
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
dependencies: dependencies:
@ -2053,6 +2085,14 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true dev: true
/vscode-oniguruma/1.7.0:
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
dev: true
/vscode-textmate/5.2.0:
resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==}
dev: true
/webidl-conversions/3.0.1: /webidl-conversions/3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: true dev: true

View file

@ -60,13 +60,18 @@ await Promise.all(
}), }),
esbuild.build({ esbuild.build({
...commonOptions, ...commonOptions,
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
define: {
"window": "unsafeWindow",
...(commonOptions?.define)
},
outfile: "dist/Vencord.user.js", outfile: "dist/Vencord.user.js",
banner: { banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`) js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`)
}, },
footer: { footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local // UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
js: "Object.defineProperty(window,'Vencord',{get:()=>Vencord});" js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
}, },
}) })
] ]

View file

@ -39,6 +39,7 @@ const {
getDarwinDirs, getDarwinDirs,
getLinuxDirs, getLinuxDirs,
ENTRYPOINT, ENTRYPOINT,
question
} = require("./common"); } = require("./common");
switch (process.platform) { switch (process.platform) {
@ -62,15 +63,14 @@ async function install(installations) {
// Attempt to give flatpak perms // Attempt to give flatpak perms
if (selected.isFlatpak) { if (selected.isFlatpak) {
try { try {
const { branch } = selected;
const cwd = process.cwd(); const cwd = process.cwd();
const globalCmd = `flatpak override ${branch} --filesystem=${cwd}`; const globalCmd = `flatpak override ${selected.branch} --filesystem=${cwd}`;
const userCmd = `flatpak override --user ${branch} --filesystem=${cwd}`; const userCmd = `flatpak override --user ${selected.branch} --filesystem=${cwd}`;
const cmd = selected.location.startsWith("/home") const cmd = selected.location.startsWith("/home")
? userCmd ? userCmd
: globalCmd; : globalCmd;
execSync(cmd); execSync(cmd);
console.log("Successfully gave write perms to Discord Flatpak."); console.log("Gave write perms to Discord Flatpak.");
} catch (e) { } catch (e) {
console.log("Failed to give write perms to Discord Flatpak."); console.log("Failed to give write perms to Discord Flatpak.");
console.log( console.log(
@ -79,6 +79,29 @@ async function install(installations) {
); );
process.exit(1); process.exit(1);
} }
const answer = await question(
`Would you like to allow ${selected.branch} to talk to org.freedesktop.Flatpak?\n` +
"This is essentially full host access but necessary to spawn git. Without it, the updater will not work\n" +
"Consider using the http based updater (using the gui installer) instead if you want to maintain the sandbox.\n" +
"[y/N]: "
);
if (["y", "yes", "yeah"].includes(answer.toLowerCase())) {
try {
const globalCmd = `flatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
const userCmd = `flatpak override --user ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
const cmd = selected.location.startsWith("/home")
? userCmd
: globalCmd;
execSync(cmd);
console.log("Sucessfully gave talk permission");
} catch (err) {
console.error("Failed to give talk permission\n", err);
}
} else {
console.log(`Not giving full host access. If you change your mind later, you can run:\nflatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`);
}
} }
for (const version of selected.versions) { for (const version of selected.versions) {

69
src/api/MessagePopover.ts Normal file
View file

@ -0,0 +1,69 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Logger from "@utils/Logger";
import { Channel, Message } from "discord-types/general";
import type { MouseEventHandler } from "react";
const logger = new Logger("MessagePopover");
export interface ButtonItem {
key?: string,
label: string,
icon: React.ComponentType<any>,
message: Message,
channel: Channel,
onClick?: MouseEventHandler<HTMLButtonElement>,
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
}
export type getButtonItem = (message: Message) => ButtonItem | null;
export const buttons = new Map<string, getButtonItem>();
export function addButton(
identifier: string,
item: getButtonItem,
) {
buttons.set(identifier, item);
}
export function removeButton(identifier: string) {
buttons.delete(identifier);
}
export function _buildPopoverElements(
msg: Message,
makeButton: (item: ButtonItem) => React.ComponentType
) {
const items = [] as React.ComponentType[];
for (const [identifier, getItem] of buttons.entries()) {
try {
const item = getItem(msg);
if (item) {
item.key ??= identifier;
items.push(makeButton(item));
}
} catch (err) {
logger.error(`[${identifier}]`, err);
}
}
return items;
}

View file

@ -21,6 +21,7 @@ import * as $Commands from "./Commands";
import * as $DataStore from "./DataStore"; import * as $DataStore from "./DataStore";
import * as $MessageAccessories from "./MessageAccessories"; import * as $MessageAccessories from "./MessageAccessories";
import * as $MessageEventsAPI from "./MessageEvents"; import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices"; import * as $Notices from "./Notices";
import * as $ServerList from "./ServerList"; import * as $ServerList from "./ServerList";
@ -59,6 +60,10 @@ const DataStore = $DataStore;
* An API allowing you to add custom components as message accessories * An API allowing you to add custom components as message accessories
*/ */
const MessageAccessories = $MessageAccessories; const MessageAccessories = $MessageAccessories;
/**
* An API allowing you to add custom buttons in the message popover
*/
const MessagePopover = $MessagePopover;
/** /**
* An API allowing you to add badges to user profiles * An API allowing you to add badges to user profiles
*/ */
@ -68,4 +73,4 @@ const Badges = $Badges;
*/ */
const ServerList = $ServerList; const ServerList = $ServerList;
export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, Notices, ServerList }; export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList };

View file

@ -141,14 +141,19 @@ export const Settings = makeProxy(settings);
* Settings hook for React components. Returns a smart settings * Settings hook for React components. Returns a smart settings
* object that automagically triggers a rerender if any properties * object that automagically triggers a rerender if any properties
* are altered * are altered
* @param paths An optional list of paths to whitelist for rerenders
* @returns Settings * @returns Settings
*/ */
export function useSettings() { export function useSettings(paths?: string[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {}); const [, forceUpdate] = React.useReducer(() => ({}), {});
const onUpdate: SubscriptionCallback = paths
? (value, path) => paths.includes(path) && forceUpdate()
: forceUpdate;
React.useEffect(() => { React.useEffect(() => {
subscriptions.add(forceUpdate); subscriptions.add(onUpdate);
return () => void subscriptions.delete(forceUpdate); return () => void subscriptions.delete(onUpdate);
}, []); }, []);
return Settings; return Settings;

View file

@ -196,7 +196,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<Forms.FormSection> <Forms.FormSection>
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent"> <ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
<plugin.settingsAboutComponent /> <plugin.settingsAboutComponent tempSettings={tempSettings} />
</ErrorBoundary> </ErrorBoundary>
</Forms.FormSection> </Forms.FormSection>
</div> </div>

View file

@ -57,7 +57,8 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
{themeLinks.map(link => ( {themeLinks.map(link => (
<Card style={{ <Card style={{
padding: ".5em", padding: ".5em",
marginBottom: ".5em" marginBottom: ".5em",
marginTop: ".5em"
}} key={link}> }} key={link}>
<Forms.FormTitle tag="h5" style={{ <Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word" overflowWrap: "break-word"
@ -95,21 +96,21 @@ export default ErrorBoundary.wrap(function () {
}}> }}>
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle> <Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText> <Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>Be careful to use the raw links or github.io links!</Forms.FormText> <Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
<Forms.FormDivider /> <Forms.FormDivider />
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle> <Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
<div> <div style={{ marginBottom: ".5em" }}>
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes"> <Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
BetterDiscord Themes BetterDiscord Themes
</Link> </Link>
<Link href="https://github.com/search?q=discord+theme">Github</Link> <Link href="https://github.com/search?q=discord+theme">GitHub</Link>
</div> </div>
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText> <Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
<Forms.FormText>In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button</Forms.FormText> <Forms.FormText>In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button</Forms.FormText>
<Forms.FormText> <Forms.FormText>
If the theme has configuration that requires you to edit the file: If the theme has configuration that requires you to edit the file:
<ul> <ul>
<li> Make a github account</li> <li> Make a <Link href="https://github.com/signup">GitHub</Link> account</li>
<li> Click the fork button on the top right</li> <li> Click the fork button on the top right</li>
<li> Edit the file</li> <li> Edit the file</li>
<li> Use the link to your own repository instead</li> <li> Use the link to your own repository instead</li>

View file

@ -179,7 +179,7 @@ function Newer(props: CommonProps) {
} }
function Updater() { function Updater() {
const [repo, err, repoPending] = useAwaiter(getRepo, "Loading..."); const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
React.useEffect(() => { React.useEffect(() => {
if (err) if (err)

View file

@ -27,7 +27,9 @@ import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common";
const st = (style: string) => `vcSettings${style}`; const st = (style: string) => `vcSettings${style}`;
function VencordSettings() { function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading..."); const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
fallbackValue: "Loading..."
});
const settings = useSettings(); const settings = useSettings();
const [donateImage] = React.useState( const [donateImage] = React.useState(
@ -87,8 +89,8 @@ function VencordSettings() {
<Switch <Switch
value={settings.useQuickCss} value={settings.useQuickCss}
onChange={(v: boolean) => settings.useQuickCss = v} onChange={(v: boolean) => settings.useQuickCss = v}
note="Loads styles from your QuickCss file"> note="Loads styles from your QuickCSS file">
Use QuickCss Use QuickCSS
</Switch> </Switch>
{!IS_WEB && ( {!IS_WEB && (
<React.Fragment> <React.Fragment>
@ -101,8 +103,8 @@ function VencordSettings() {
<Switch <Switch
value={settings.notifyAboutUpdates} value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v} onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a Toast on StartUp"> note="Shows a toast on startup">
Get notified about new Updates Get notified about new updates
</Switch> </Switch>
</React.Fragment> </React.Fragment>
)} )}
@ -129,7 +131,7 @@ function DonateCard({ image }: DonateCardProps) {
<div> <div>
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle> <Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
<Forms.FormText> <Forms.FormText>
Please consider supporting the Development of Vencord by donating! Please consider supporting the development of Vencord by donating!
</Forms.FormText> </Forms.FormText>
<DonateButton style={{ transform: "translateX(-1em)" }} /> <DonateButton style={{ transform: "translateX(-1em)" }} />
</div> </div>

View file

@ -89,8 +89,12 @@ export function initIpc(mainWindow: BrowserWindow) {
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
const win = new BrowserWindow({ const win = new BrowserWindow({
title: "QuickCss Editor", title: "QuickCss Editor",
autoHideMenuBar: true,
darkTheme: true,
webPreferences: { webPreferences: {
preload: join(__dirname, "preload.js"), preload: join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false
} }
}); });
await win.loadURL(`data:text/html;base64,${monacoHtml}`); await win.loadURL(`data:text/html;base64,${monacoHtml}`);

View file

@ -28,10 +28,13 @@ const VENCORD_SRC_DIR = join(__dirname, "..");
const execFile = promisify(cpExecFile); const execFile = promisify(cpExecFile);
const isFlatpak = Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
function git(...args: string[]) { function git(...args: string[]) {
return execFile("git", args, { const opts = { cwd: VENCORD_SRC_DIR };
cwd: VENCORD_SRC_DIR
}); if (isFlatpak) return execFile("flatpak-spawn", ["--host", "git", ...args], opts);
else return execFile("git", args, opts);
} }
async function getRepo() { async function getRepo() {
@ -61,9 +64,13 @@ async function pull() {
} }
async function build() { async function build() {
const res = await execFile("node", ["scripts/build/build.mjs"], { const opts = { cwd: VENCORD_SRC_DIR };
cwd: VENCORD_SRC_DIR
}); let res;
if (isFlatpak) res = await execFile("flatpak-spawn", ["--host", "node", "scripts/build/build.mjs"], opts);
else res = await execFile("node", ["scripts/build/build.mjs"], opts);
return !res.stderr.includes("Build failed"); return !res.stderr.includes("Build failed");
} }

View file

@ -42,98 +42,122 @@ require.main!.filename = join(asarPath, discordPkg.main);
// @ts-ignore Untyped method? Dies from cringe // @ts-ignore Untyped method? Dies from cringe
app.setAppPath(asarPath); app.setAppPath(asarPath);
// Repatch after host updates on Windows if (!process.argv.includes("--vanilla")) {
if (process.platform === "win32") // Repatch after host updates on Windows
require("./patchWin32Updater"); if (process.platform === "win32")
require("./patchWin32Updater");
class BrowserWindow extends electron.BrowserWindow { class BrowserWindow extends electron.BrowserWindow {
constructor(options: BrowserWindowConstructorOptions) { constructor(options: BrowserWindowConstructorOptions) {
if (options?.webPreferences?.preload && options.title) { if (options?.webPreferences?.preload && options.title) {
const original = options.webPreferences.preload; const original = options.webPreferences.preload;
options.webPreferences.preload = join(__dirname, "preload.js"); options.webPreferences.preload = join(__dirname, "preload.js");
options.webPreferences.sandbox = false; options.webPreferences.sandbox = false;
process.env.DISCORD_PRELOAD = original; process.env.DISCORD_PRELOAD = original;
super(options); super(options);
initIpc(this); initIpc(this);
} else super(options); } else super(options);
}
}
Object.assign(BrowserWindow, electron.BrowserWindow);
// esbuild may rename our BrowserWindow, which leads to it being excluded
// from getFocusedWindow(), so this is necessary
// https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
// Replace electrons exports with our custom BrowserWindow
const electronPath = require.resolve("electron");
delete require.cache[electronPath]!.exports;
require.cache[electronPath]!.exports = {
...electron,
BrowserWindow
};
// Patch appSettings to force enable devtools
onceDefined(global, "appSettings", s =>
s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true)
);
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
electron.app.whenReady().then(() => {
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
case "renderer.js.map":
case "preload.js.map":
case "patcher.js.map": // doubt
cb(join(__dirname, url));
break;
default:
cb({ statusCode: 403 });
} }
}); }
Object.assign(BrowserWindow, electron.BrowserWindow);
// esbuild may rename our BrowserWindow, which leads to it being excluded
// from getFocusedWindow(), so this is necessary
// https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
try { // Replace electrons exports with our custom BrowserWindow
const settings = JSON.parse(readSettings()); const electronPath = require.resolve("electron");
if (settings.enableReactDevtools) delete require.cache[electronPath]!.exports;
installExt("fmkadmapgofadopljbjfkapdkoienihi") require.cache[electronPath]!.exports = {
.then(() => console.info("[Vencord] Installed React Developer Tools")) ...electron,
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err)); BrowserWindow
} catch { } };
// Patch appSettings to force enable devtools
onceDefined(global, "appSettings", s =>
s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true)
);
// Remove CSP process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) { electron.app.whenReady().then(() => {
let patchedHeader = headers[header][0]; // Source Maps! Maybe there's a better way but since the renderer is executed
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src"]) { // from a string I don't think any other form of sourcemaps would work
patchedHeader = patchedHeader.replace(new RegExp(`${directive}.+?;`), `${directive} * blob: data: 'unsafe-inline';`); electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
case "renderer.js.map":
case "preload.js.map":
case "patcher.js.map": // doubt
cb(join(__dirname, url));
break;
default:
cb({ statusCode: 403 });
} }
// TODO: Restrict this to only imported packages with fixed version. });
// Perhaps auto generate with esbuild
patchedHeader = patchedHeader.replace(/script-src.+?(?=;)/, "$& 'unsafe-eval' https://unpkg.com https://cdnjs.cloudflare.com");
headers[header] = [patchedHeader];
}
}
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, url }, cb) => { try {
if (responseHeaders) { const settings = JSON.parse(readSettings());
patchCsp(responseHeaders, "content-security-policy"); if (settings.enableReactDevtools)
patchCsp(responseHeaders, "content-security-policy-report-only"); installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { }
// Fix hosts that don't properly set the content type, such as
// raw.githubusercontent.com // Remove CSP
if (url.endsWith(".css")) type PolicyResult = Record<string, string[]>;
responseHeaders["content-type"] = ["text/css"];
const parsePolicy = (policy: string): PolicyResult => {
const result: PolicyResult = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
} }
cb({ cancel: false, responseHeaders });
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders, "content-security-policy");
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet")
responseHeaders["content-type"] = ["text/css"];
}
cb({ cancel: false, responseHeaders });
});
}); });
}); } else {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
}
console.log("[Vencord] Loading original Discord app.asar"); console.log("[Vencord] Loading original Discord app.asar");
// Legacy Vencord Injector requires "../app.asar". However, because we // Legacy Vencord Injector requires "../app.asar". However, because we

View file

@ -0,0 +1,33 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "MessagePopoverAPI",
description: "API to add buttons to message popovers.",
authors: [Devs.KingFish],
patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: {
match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,90}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/,
replace: "$1...Vencord.Api.MessagePopover._buildPopoverElements($2,$4),$3"
}
}],
});

View file

@ -30,7 +30,7 @@ const assetManager = mapMangledModuleLazy(
} }
); );
const rpcManager = findByCodeLazy(".APPLICATION_RPC("); const lookupRpcApp = findByCodeLazy(".APPLICATION_RPC(");
async function lookupAsset(applicationId: string, key: string): Promise<string> { async function lookupAsset(applicationId: string, key: string): Promise<string> {
return (await assetManager.getAsset(applicationId, [key, undefined]))[0]; return (await assetManager.getAsset(applicationId, [key, undefined]))[0];
@ -39,7 +39,7 @@ async function lookupAsset(applicationId: string, key: string): Promise<string>
const apps: any = {}; const apps: any = {};
async function lookupApp(applicationId: string): Promise<string> { async function lookupApp(applicationId: string): Promise<string> {
const socket: any = {}; const socket: any = {};
await rpcManager.lookupApp(socket, applicationId); await lookupRpcApp(socket, applicationId);
return socket.application; return socket.application;
} }

View file

@ -71,8 +71,8 @@ export default definePlugin({
"canUseEmojisEverywhere" "canUseEmojisEverywhere"
].map(func => { ].map(func => {
return { return {
match: new RegExp(`${func}:function\\(.+?}`), match: new RegExp(`${func}:function\\(.+?\\{`),
replace: `${func}:function(e){return true;}` replace: "$&return true;"
}; };
}) })
}, },
@ -80,8 +80,8 @@ export default definePlugin({
find: "canUseAnimatedEmojis:function", find: "canUseAnimatedEmojis:function",
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true, predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
replacement: { replacement: {
match: /canUseStickersEverywhere:function\(.+?}/, match: /canUseStickersEverywhere:function\(.+?\{/,
replace: "canUseStickersEverywhere:function(e){return true;}" replace: "$&return true;"
}, },
}, },
{ {
@ -101,8 +101,8 @@ export default definePlugin({
"canStreamMidQuality" "canStreamMidQuality"
].map(func => { ].map(func => {
return { return {
match: new RegExp(`${func}:function\\(.+?}`), match: new RegExp(`${func}:function\\(.+?\\{`),
replace: `${func}:function(e){return true;}` replace: "$&return true;"
}; };
}) })
}, },

View file

@ -17,11 +17,10 @@
*/ */
import { get, set } from "@api/DataStore"; import { get, set } from "@api/DataStore";
import { addButton, removeButton } from "@api/MessagePopover";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import Logger from "@utils/Logger";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { ChannelStore, FluxDispatcher } from "@webpack/common"; import { ChannelStore, FluxDispatcher } from "@webpack/common";
import { Message } from "discord-types/general";
let style: HTMLStyleElement; let style: HTMLStyleElement;
@ -49,13 +48,7 @@ export default definePlugin({
name: "HideAttachments", name: "HideAttachments",
description: "Hide attachments and Embeds for individual messages via hover button", description: "Hide attachments and Embeds for individual messages via hover button",
authors: [Devs.Ven], authors: [Devs.Ven],
patches: [{ dependencies: ["MessagePopoverAPI"],
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: {
match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,40}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/,
replace: "$1Vencord.Plugins.plugins.HideAttachments.renderButton($2, $4),$3"
}
}],
async start() { async start() {
style = document.createElement("style"); style = document.createElement("style");
@ -64,11 +57,26 @@ export default definePlugin({
await getHiddenMessages(); await getHiddenMessages();
await this.buildCss(); await this.buildCss();
addButton("HideAttachments", msg => {
if (!msg.attachments.length && !msg.embeds.length) return null;
const isHidden = hiddenMessages.has(msg.id);
return {
label: isHidden ? "Show Attachments" : "Hide Attachments",
icon: isHidden ? ImageVisible : ImageInvisible,
message: msg,
channel: ChannelStore.getChannel(msg.channel_id),
onClick: () => this.toggleHide(msg.id)
};
});
}, },
stop() { stop() {
style.remove(); style.remove();
hiddenMessages.clear(); hiddenMessages.clear();
removeButton("HideAttachments");
}, },
async buildCss() { async buildCss() {
@ -86,26 +94,6 @@ export default definePlugin({
`; `;
}, },
renderButton(msg: Message, makeItem: (data: any) => React.ComponentType) {
try {
if (!msg.attachments.length && !msg.embeds.length) return null;
const isHidden = hiddenMessages.has(msg.id);
return makeItem({
key: "HideAttachments",
label: isHidden ? "Show Attachments" : "Hide Attachments",
icon: isHidden ? ImageVisible : ImageInvisible,
message: msg,
channel: ChannelStore.getChannel(msg.channel_id),
onClick: () => this.toggleHide(msg.id)
});
} catch (err) {
new Logger("HideAttachments").error(err);
return null;
}
},
async toggleHide(id: string) { async toggleHide(id: string) {
const ids = await getHiddenMessages(); const ids = await getHiddenMessages();
if (!ids.delete(id)) if (!ids.delete(id))

View file

@ -1,178 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as DataStore from "@api/DataStore";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
interface MatchAndReplace {
match: RegExp;
replace: string;
}
/** Used to re-render the Registered Games tab to update how our button looks like */
const RunningGameStoreModule = findByPropsLazy("IgnoreActivities_reRenderGames");
let ignoredActivitiesCache: string[] = [];
export default definePlugin({
name: "IgnoreActivities",
authors: [Devs.Nuckyz],
description: "Ignore certain activities (like games) from showing up on your status. You can configure which ones are ignored from the Registered Games tab.",
patches: [{
find: ".Messages.SETTINGS_GAMES_OVERLAY_ON",
replacement: [{
match: /;(.\.renderOverlayToggle=function\(\).+?\)};)/,
replace: (_, mod) => {
/** Modify the renderOverlayToggle button to remove unneded stuff and render the component the way we want */
const renderIgnoreActivitiesToggle = ([
/** Remove overlay warn related stuff */
{ match: /,.{1,2}=.{1,2}\.overlayWarn/, replace: "" },
{ match: /,.{1,2}=.{1,2}\?\(0,.{1,2}\.jsx\)\(.{1,20}Messages\.SETTINGS_GAMES_OVERLAY_WARNING.{1,100}null/, replace: "" },
/** Remove overlay status related stuff */
{ match: /,.{1,2}=.{1,2}\?.{1,50}Messages\.SETTINGS_GAMES_OVERLAY_OFF/, replace: "" },
{ match: /[^[]{1,2},\(0,.{1,2}\.jsx\)\("div".{1,20}\(\)\.overlayStatusText.+}\),/, replace: "" },
/** Change the method name to renderIgnoreActivitiesToggle */
{ match: /renderOverlayToggle/, replace: "renderIgnoreActivitiesToggle" },
/** Create an easily accessable variable to use the game props and then replace the boolean to determine if the button is activated or not with our custom function */
{ match: /((.)=this\.props\.game)(.{1,70})=.{1,2}overlay/, replace: "$1,IgnoreActivities_gameProps=$2$3=Vencord.Plugins.plugins.IgnoreActivities.isActivityEnabled(IgnoreActivities_gameProps)" },
/** Change the handler for clicking the button */
{ match: /.\.handleOverlayToggle/, replace: "() => Vencord.Plugins.plugins.IgnoreActivities.handleActivityToggle(IgnoreActivities_gameProps)" },
/** Change the button on component to our custom */
{ match: /(\(0,.{1,2}\.jsx\)\()(.{2})\..(.{1,50}\.overlayToggleIconOn)/, replace: "$1$2.IgnoreActivities_toggleOn$3" },
/** Change the button off component to our custom */
{ match: /(\(0,.{1,2}\.jsx\)\()(.{2})\..{1}(.{1,50}\.overlayToggleIconOff)/, replace: "$1$2.IgnoreActivities_toggleOff$3" },
/** Change the tooltip text */
{ match: /text:.{2}\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY/, replace: 'text:"Toggle activity"' },
/** Change the aria-label text */
{ match: /"aria-label":.{2}\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY/, replace: '"aria-label":"Toggle activity"' }
] as MatchAndReplace[])
.reduce((current, { match, replace }) => current.replace(match, replace), mod);
/** Return the default renderOverlayToggle and our custom one */
return `;${mod}${renderIgnoreActivitiesToggle}`;
}
}, {
/** Render our ignore activity component */
match: /(this.renderLastPlayed\(\)]}\),this.renderOverlayToggle\(\))/,
replace: "$1,this.renderIgnoreActivitiesToggle()"
}]
}, {
/** Patch the RunningGameStore to export the method to re-render the Registered Games tab */
find: '.displayName="RunningGameStore"',
replacement: {
match: /(.:\(\)=>.{2})(.+function (.{2})\(\){.+\.dispatch\({type:"RUNNING_GAMES_CHANGE")/,
replace: "$1,IgnoreActivities_reRenderGames:()=>$3$2"
}
}, {
find: "M8.67872 19H11V21H7V23H17V21H13V19H20C21.103 19 22 18.104 22 17V6C22 5.89841 21.9924 5.79857 21.9777 5.70101L20 7.67872V15H12.6787L8.67872 19ZM13.1496 6H4V15H4.14961L2.00515 17.1445C2.00174 17.0967 2 17.0486 2 17V6C2 4.897 2.897 4 4 4H15.1496L13.1496 6Z",
replacement: {
match: /(.:\(\)=>.)(.+)(function (.)\(.{1,10}\.width.+\)\)})/s,
replace: (_, exports, restOfFunction, component) => {
/** Modify the overlayToggleOff component to how we want */
const renderIgnoreActivitiesToggleOff = ([
/** Change the method name to IgnoreActivities_toggleOffToExport */
{ match: /function ./, replace: "function IgnoreActivities_toggleOffToExport" },
/** Change the svg path to our custom one */
{ match: /M8.67872 19H11V21H7V23H17V21H13V19H20C21.103 19 22 18.104 22 17V6C22 5.89841 21.9924 5.79857 21.9777 5.70101L20 7.67872V15H12.6787L8.67872 19ZM13.1496 6H4V15H4.14961L2.00515 17.1445C2.00174 17.0967 2 17.0486 2 17V6C2 4.897 2.897 4 4 4H15.1496L13.1496 6Z/, replace: "M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z" },
/** Modify the view box to not cut our svg */
{ match: /viewBox:"0 0 24 24"/, replace: 'viewBox:"0 0 32 26"' },
/** Change the rectangle coordinates to match the middle of our svg */
{ match: /x:"2"/, replace: 'x:"3"' },
{ match: /y:"20"/, replace: 'y:"26"' },
] as MatchAndReplace[])
.reduce((current, { match, replace }) => current.replace(match, replace), component);
/** Export our custom svg */
return `${exports},IgnoreActivities_toggleOff:()=>IgnoreActivities_toggleOffToExport${restOfFunction}${component}${renderIgnoreActivitiesToggleOff}`;
}
}
}, {
find: "M4 2.5C2.897 2.5 2 3.397 2 4.5V15.5C2 16.604 2.897 17.5 4 17.5H11V19.5H7V21.5H17V19.5H13V17.5H20C21.103 17.5 22 16.604 22 15.5V4.5C22 3.397 21.103 2.5 20 2.5H4ZM20 4.5V13.5H4V4.5H20Z",
replacement: {
match: /(.:\(\)=>.)(.+)(function (.)\(.{1,10}\.width.+\)\)})/,
replace: (_, exports, restOfFunction, component) => {
/** Modify the overlayToggleOn svg to how we want */
const renderIgnoreActivitiesToggleOn = ([
/** Change the method name to IgnoreActivities_toggleOnToExport */
{ match: /function ./, replace: "function IgnoreActivities_toggleOnToExport" },
/** Change the svg path to our custom one */
{ match: /M4 2.5C2.897 2.5 2 3.397 2 4.5V15.5C2 16.604 2.897 17.5 4 17.5H11V19.5H7V21.5H17V19.5H13V17.5H20C21.103 17.5 22 16.604 22 15.5V4.5C22 3.397 21.103 2.5 20 2.5H4ZM20 4.5V13.5H4V4.5H20Z/, replace: "M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z" },
/** Modify the view box to not cut our svg */
{ match: /viewBox:"0 0 24 24"/, replace: 'viewBox:"0 0 32 26"' },
] as MatchAndReplace[])
.reduce((current, { match, replace }) => current.replace(match, replace), component);
/** Export our custom svg */
return `${exports},IgnoreActivities_toggleOn:()=>IgnoreActivities_toggleOnToExport${restOfFunction}${component}${renderIgnoreActivitiesToggleOn}`;
}
}
}, {
/** Patch the LocalActivityStore to filter our ignored activities before they get pushed into the array */
find: '.displayName="LocalActivityStore"',
replacement: {
match: /((.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?;)/,
replace: "$1$2=$2.filter(Vencord.Plugins.plugins.IgnoreActivities.isActivityEnabled);"
}
}],
async start() {
ignoredActivitiesCache = (await DataStore.get<string[]>("IgnoreActivities_ignoredActivities")) ?? [];
if (ignoredActivitiesCache.length !== 0) {
const gamesSeen: Record<string, any>[] = RunningGameStoreModule.Z.getGamesSeen();
for (const [index, ignoredActivity] of ignoredActivitiesCache.entries()) {
if (!gamesSeen.some(game => (game.id !== undefined && game.id === ignoredActivity) || game.exePath === ignoredActivity)) {
ignoredActivitiesCache.splice(index, 1);
}
}
await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache);
}
},
isActivityEnabled(props: Record<string, any>) {
/** LocalActivityStore games have a "type" prop */
if ("type" in props) {
if (props.application_id !== undefined) return !ignoredActivitiesCache.includes(props.application_id);
else {
const exePath = RunningGameStoreModule.Z.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) return !ignoredActivitiesCache.includes(exePath);
}
}
/** Registered Games tab games have an "exePath" prop */
else if ("exePath" in props) {
if (props.id !== undefined) return !ignoredActivitiesCache.includes(props.id);
else return !ignoredActivitiesCache.includes(props.exePath);
}
return true;
},
async handleActivityToggle(props: Record<string, any>) {
const id = props.id ?? props.exePath;
if (id === undefined) return;
if (ignoredActivitiesCache.includes(id)) ignoredActivitiesCache.splice(ignoredActivitiesCache.indexOf(id, 1));
else ignoredActivitiesCache.push(id);
RunningGameStoreModule.IgnoreActivities_reRenderGames();
await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache);
}
});

View file

@ -0,0 +1,220 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as DataStore from "@api/DataStore";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/misc";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Tooltip } from "webpack/common";
enum ActivitiesTypes {
Game,
Embedded
}
interface IgnoredActivity {
id: string;
type: ActivitiesTypes;
}
const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
const PreviewBadgeClasses = findByPropsLazy("previewBadge", "previewBadgeIcon");
const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
const RunningGameStore = findByPropsLazy("getRunningGames", "getGamesSeen");
function ToggleIconOff() {
return (
<svg
className={RegisteredGamesClasses.overlayToggleIconOff}
height="24"
width="24"
viewBox="0 0 32 26"
aria-hidden={true}
role="img"
>
<g
fill="none"
fillRule="evenodd"
>
<path
className={RegisteredGamesClasses.fill}
fill="currentColor"
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
/>
<rect
className={RegisteredGamesClasses.fill}
x="3"
y="26"
width="26"
height="2"
transform="rotate(-45 2 20)"
/>
</g>
</svg>
);
}
function ToggleIconOn() {
return (
<svg
className={RegisteredGamesClasses.overlayToggleIconOn}
height="24"
width="24"
viewBox="0 0 32 26"
>
<path
className={RegisteredGamesClasses.fill}
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
/>
</svg>
);
}
function ToggleActivityComponent({ activity }: { activity: IgnoredActivity; }) {
const forceUpdate = useForceUpdater();
return (
<Tooltip text="Toggle activity">
{({ onMouseLeave, onMouseEnter }) => (
<div
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
className={RegisteredGamesClasses.overlayToggleIcon}
role="button"
aria-label="Toggle activity"
tabIndex={0}
onClick={e => handleActivityToggle(e, activity, forceUpdate)}
>
{
ignoredActivitiesCache.has(activity.id)
? <ToggleIconOff />
: <ToggleIconOn />
}
</div>
)}
</Tooltip>
);
}
function ToggleActivityComponentWithBackground({ activity }: { activity: IgnoredActivity; }) {
return (
<div
className={`${PreviewBadgeClasses.previewBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
style={{ padding: "0 2px" }}
>
<ToggleActivityComponent activity={activity} />
</div>
);
}
function handleActivityToggle(e: React.MouseEvent<HTMLDivElement, MouseEvent>, activity: IgnoredActivity, forceUpdateComponent: () => void) {
e.stopPropagation();
if (ignoredActivitiesCache.has(activity.id)) ignoredActivitiesCache.delete(activity.id);
else ignoredActivitiesCache.set(activity.id, activity);
forceUpdateComponent();
saveCacheToDatastore();
}
async function saveCacheToDatastore() {
await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache);
}
let ignoredActivitiesCache = new Map<IgnoredActivity["id"], IgnoredActivity>();
export default definePlugin({
name: "IgnoreActivities",
authors: [Devs.Nuckyz],
description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.",
patches: [{
find: ".Messages.SETTINGS_GAMES_OVERLAY_ON",
replacement: {
match: /(this.renderLastPlayed\(\)]}\),this.renderOverlayToggle\(\))/,
replace: "$1,Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(this.props)"
}
}, {
find: ".Messages.NEW,name",
replacement: {
match: /\(\)\.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?<props>.)\.name}\):null/,
replace: "$&,Vencord.Plugins.plugins.IgnoreActivities.renderToggleActivityButton($<props>)"
}
}, {
find: '.displayName="LocalActivityStore"',
replacement: {
match: /((.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?;)/,
replace: "$1$2=$2.filter(Vencord.Plugins.plugins.IgnoreActivities.isActivityEnabled);"
}
}],
async start() {
const ignoredActivitiesData = await DataStore.get<string[] | Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities") ?? new Map<IgnoredActivity["id"], IgnoredActivity>();
/** Migrate old data */
if (Array.isArray(ignoredActivitiesData)) {
for (const id of ignoredActivitiesData) {
ignoredActivitiesCache.set(id, { id, type: ActivitiesTypes.Game });
}
await saveCacheToDatastore();
} else ignoredActivitiesCache = ignoredActivitiesData;
if (ignoredActivitiesCache.size !== 0) {
const gamesSeen: { id?: string; exePath: string; }[] = RunningGameStore.getGamesSeen();
for (const ignoredActivity of ignoredActivitiesCache.values()) {
if (ignoredActivity.type !== ActivitiesTypes.Game) continue;
if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {
/** Custom added game which no longer exists */
ignoredActivitiesCache.delete(ignoredActivity.id);
}
}
await saveCacheToDatastore();
}
},
renderToggleGameActivityButton(props: { game: { id?: string; exePath: string; } | null; }) {
if (!props.game) return (null);
return (
<ErrorBoundary noop>
<ToggleActivityComponent activity={{ id: props.game.id ?? props.game.exePath, type: ActivitiesTypes.Game }} />
</ErrorBoundary>
);
},
renderToggleActivityButton(props: { id: string; }) {
return (
<ErrorBoundary noop>
<ToggleActivityComponentWithBackground activity={{ id: props.id, type: ActivitiesTypes.Embedded }} />
</ErrorBoundary>
);
},
isActivityEnabled(props: { type: number; application_id?: string; name?: string; }) {
if (props.type === 0) {
if (props.application_id !== undefined) return !ignoredActivitiesCache.has(props.application_id);
else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) return !ignoredActivitiesCache.has(exePath);
}
}
return true;
},
});

View file

@ -40,7 +40,7 @@ interface PreviousChannel {
export default definePlugin({ export default definePlugin({
name: "KeepCurrentChannel", name: "KeepCurrentChannel",
description: "Attempt to navigate the channel you were in before switching accounts or loading Discord.", description: "Attempt to navigate to the channel you were in before switching accounts or loading Discord.",
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz],
isSwitchingAccount: false, isSwitchingAccount: false,

View file

@ -35,11 +35,13 @@ function MemberCount() {
if (!c) return null; if (!c) return null;
let total = String(c[0]); let total = c[0].toLocaleString();
if (total === "0" && c[1] > 0) { if (total === "0" && c[1] > 0) {
total = "Loading..."; total = "Loading...";
} }
const online = c[1].toLocaleString();
return ( return (
<Flex id="vc-membercount" style={{ <Flex id="vc-membercount" style={{
marginTop: "1em", marginTop: "1em",
@ -49,7 +51,7 @@ function MemberCount() {
alignContent: "center", alignContent: "center",
gap: 0 gap: 0
}}> }}>
<Tooltip text={`${c[1]} Online`} position="bottom"> <Tooltip text={`${online} Online`} position="bottom">
{props => ( {props => (
<div {...props}> <div {...props}>
<span <span
@ -62,11 +64,11 @@ function MemberCount() {
marginRight: "0.5em" marginRight: "0.5em"
}} }}
/> />
<span style={{ color: "var(--status-green-600)" }}>{c[1]}</span> <span style={{ color: "var(--status-green-600)" }}>{online}</span>
</div> </div>
)} )}
</Tooltip> </Tooltip>
<Tooltip text={`${c[0] || "?"} Total Members`} position="bottom"> <Tooltip text={`${total} Total Members`} position="bottom">
{props => ( {props => (
<div {...props}> <div {...props}>
<span <span
@ -91,7 +93,7 @@ function MemberCount() {
export default definePlugin({ export default definePlugin({
name: "MemberCount", name: "MemberCount",
description: "Shows the amount of online & total members in the server member list", description: "Shows the amount of online & total members in the server member list",
authors: [Devs.Ven], authors: [Devs.Ven, Devs.Commandtechno],
patches: [{ patches: [{
find: ".isSidebarVisible,", find: ".isSidebarVisible,",

View file

@ -0,0 +1,35 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 OpenAsar
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "NSFWGateBypass",
description: "Allows you to access NSFW channels without setting/verifying your age",
authors: [Devs.Commandtechno],
patches: [
{
find: ".nsfwAllowed=null",
replacement: {
match: /(\w+)\.nsfwAllowed=/,
replace: "$1.nsfwAllowed=true;",
},
},
],
});

View file

@ -39,11 +39,10 @@ export default function PronounsChatComponentWrapper({ message }: { message: Mes
} }
function PronounsChatComponent({ message }: { message: Message; }) { function PronounsChatComponent({ message }: { message: Message; }) {
const [result, , isPending] = useAwaiter( const [result, , isPending] = useAwaiter(() => fetchPronouns(message.author.id), {
() => fetchPronouns(message.author.id), fallbackValue: null,
null, onError: e => console.error("Fetching pronouns failed: ", e)
e => console.error("Fetching pronouns failed: ", e) });
);
// If the promise completed, the result was not "unspecified", and there is a mapping for the code, then return a span with the pronouns // If the promise completed, the result was not "unspecified", and there is a mapping for the code, then return a span with the pronouns
if (!isPending && result && result !== "unspecified" && PronounMapping[result]) { if (!isPending && result && result !== "unspecified" && PronounMapping[result]) {

View file

@ -45,11 +45,10 @@ function ProfilePronouns(
leProps: UserProfilePronounsProps; leProps: UserProfilePronounsProps;
} }
) { ) {
const [result, , isPending] = useAwaiter( const [result, , isPending] = useAwaiter(() => fetchPronouns(userId), {
() => fetchPronouns(userId), fallbackValue: null,
null, onError: e => console.error("Fetching pronouns failed: ", e),
e => console.error("Fetching pronouns failed: ", e) });
);
// If the promise completed, the result was not "unspecified", and there is a mapping for the code, then render // If the promise completed, the result was not "unspecified", and there is a mapping for the code, then render
if (!isPending && result && result !== "unspecified" && PronounMapping[result]) { if (!isPending && result && result !== "unspecified" && PronounMapping[result]) {

View file

@ -16,9 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addButton, removeButton } from "@api/MessagePopover";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findLazy } from "@webpack"; import { findLazy } from "@webpack";
import { ChannelStore } from "@webpack/common";
const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT); const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT);
@ -26,29 +28,22 @@ export default definePlugin({
name: "QuickMention", name: "QuickMention",
authors: [Devs.kemo], authors: [Devs.kemo],
description: "Adds a quick mention button to the message actions bar", description: "Adds a quick mention button to the message actions bar",
dependencies: ["MessagePopoverAPI"],
patches: [ start() {
{ addButton("QuickMention", msg => {
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", return {
replacement: { label: "Quick Mention",
match: /(null,)(.{1,3}&&!.{1,3}\?(.{1,3})\(\{key:"reply",label:.{1,10}\.Messages\.MESSAGE_ACTION_REPLY,icon:.{1,10},channel:(.+?),message:(.+?),onClick:.+?\}\))/, icon: this.Icon,
replace: (m, post, og, functionName, channelVar, messageVar) => { message: msg,
channel: ChannelStore.getChannel(msg.channel_id),
const functionSig = onClick: () => ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: `<@${msg.author.id}> ` })
`${functionName}({ };
key: "QuickMention", });
label: "Mention", },
icon: Vencord.Plugins.plugins.QuickMention.Icon, stop() {
channel: ${channelVar}, removeButton("QuickMention");
message: ${messageVar}, },
onClick: ()=> Vencord.Plugins.plugins.QuickMention.onClick(${messageVar})
})`;
return `${post}${functionSig},${og}`;
}
}
}
],
Icon: () => ( Icon: () => (
<svg <svg
@ -63,6 +58,4 @@ export default definePlugin({
/> />
</svg> </svg>
), ),
onClick: (message: any) => ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: `<@${message.author.id}> ` })
}); });

View file

@ -21,7 +21,7 @@ import definePlugin from "@utils/types";
import { Menu } from "@webpack/common"; import { Menu } from "@webpack/common";
const Engines = { const Engines = {
Google: "https://www.google.com/searchbyimage?image_url=", Google: "https://lens.google.com/uploadbyurl?url=",
Yandex: "https://yandex.com/images/search?rpt=imageview&url=", Yandex: "https://yandex.com/images/search?rpt=imageview&url=",
SauceNAO: "https://saucenao.com/search.php?url=", SauceNAO: "https://saucenao.com/search.php?url=",
IQDB: "https://iqdb.org/?url=", IQDB: "https://iqdb.org/?url=",

View file

@ -18,7 +18,7 @@
import { classes, useAwaiter } from "@utils/misc"; import { classes, useAwaiter } from "@utils/misc";
import { findLazy } from "@webpack"; import { findLazy } from "@webpack";
import { Forms, Text, UserStore } from "@webpack/common"; import { Forms, React, Text, UserStore } from "@webpack/common";
import type { KeyboardEvent } from "react"; import type { KeyboardEvent } from "react";
import { addReview, getReviews } from "../Utils/ReviewDBAPI"; import { addReview, getReviews } from "../Utils/ReviewDBAPI";
@ -27,7 +27,13 @@ import ReviewComponent from "./ReviewComponent";
const Classes = findLazy(m => typeof m.textarea === "string"); const Classes = findLazy(m => typeof m.textarea === "string");
export default function ReviewsView({ userId }: { userId: string; }) { export default function ReviewsView({ userId }: { userId: string; }) {
const [reviews, _, isLoading, refetch] = useAwaiter(() => getReviews(userId), []); const [refetchCount, setRefetchCount] = React.useState(0);
const [reviews, _, isLoading] = useAwaiter(() => getReviews(userId), {
fallbackValue: [],
deps: [refetchCount],
});
const dirtyRefetch = () => setRefetchCount(refetchCount + 1);
if (isLoading) return null; if (isLoading) return null;
@ -40,7 +46,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
}).then(res => { }).then(res => {
if (res === 0 || res === 1) { if (res === 0 || res === 1) {
(target as HTMLInputElement).value = ""; // clear the input (target as HTMLInputElement).value = ""; // clear the input
refetch(); dirtyRefetch();
} }
}); });
} }
@ -62,7 +68,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
<ReviewComponent <ReviewComponent
key={review.id} key={review.id}
review={review} review={review}
refetch={refetch} refetch={dirtyRefetch}
/> />
)} )}
{reviews?.length === 0 && ( {reviews?.length === 0 && (

View file

@ -0,0 +1,74 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ILanguageRegistration } from "@vap/shiki";
export const VPC_REPO = "Vap0r1ze/vapcord";
export const VPC_REPO_COMMIT = "88a7032a59cca40da170926651b08201ea3b965a";
export const vpcRepoAssets = `https://raw.githubusercontent.com/${VPC_REPO}/${VPC_REPO_COMMIT}/assets/shiki-codeblocks`;
export const vpcRepoGrammar = (fileName: string) => `${vpcRepoAssets}/${fileName}`;
export const vpcRepoLanguages = `${vpcRepoAssets}/languages.json`;
export interface Language {
name: string;
id: string;
devicon?: string;
grammarUrl: string,
grammar?: ILanguageRegistration["grammar"];
scopeName: string;
aliases?: string[];
custom?: boolean;
}
export interface LanguageJson {
name: string;
id: string;
fileName: string;
devicon?: string;
scopeName: string;
aliases?: string[];
}
export const languages: Record<string, Language> = {};
export const loadLanguages = async () => {
const langsJson: LanguageJson[] = await fetch(vpcRepoLanguages).then(res => res.json());
const loadedLanguages = Object.fromEntries(
langsJson.map(lang => [lang.id, {
...lang,
grammarUrl: vpcRepoGrammar(lang.fileName),
}])
);
Object.assign(languages, loadedLanguages);
};
export const getGrammar = (lang: Language): Promise<NonNullable<ILanguageRegistration["grammar"]>> => {
if (lang.grammar) return Promise.resolve(lang.grammar);
return fetch(lang.grammarUrl).then(res => res.json());
};
const aliasCache = new Map<string, Language>();
export function resolveLang(idOrAlias: string) {
if (Object.prototype.hasOwnProperty.call(languages, idOrAlias)) return languages[idOrAlias];
const lang = Object.values(languages).find(lang => lang.aliases?.includes(idOrAlias));
if (!lang) return null;
aliasCache.set(idOrAlias, lang);
return lang;
}

View file

@ -0,0 +1,119 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { shikiOnigasmSrc, shikiWorkerSrc } from "@utils/dependencies";
import { WorkerClient } from "@vap/core/ipc";
import type { IShikiTheme, IThemedToken } from "@vap/shiki";
import { dispatchTheme } from "../hooks/useTheme";
import type { ShikiSpec } from "../types";
import { getGrammar, languages, loadLanguages, resolveLang } from "./languages";
import { themes } from "./themes";
const themeUrls = Object.values(themes);
let resolveClient: (client: WorkerClient<ShikiSpec>) => void;
export const shiki = {
client: null as WorkerClient<ShikiSpec> | null,
currentTheme: null as IShikiTheme | null,
currentThemeUrl: null as string | null,
timeoutMs: 10000,
languages,
themes,
loadedThemes: new Set<string>(),
loadedLangs: new Set<string>(),
clientPromise: new Promise<WorkerClient<ShikiSpec>>(resolve => resolveClient = resolve),
init: async (initThemeUrl: string | undefined) => {
/** https://stackoverflow.com/q/58098143 */
const workerBlob = await fetch(shikiWorkerSrc).then(res => res.blob());
const client = shiki.client = new WorkerClient<ShikiSpec>(
"shiki-client",
"shiki-host",
workerBlob,
{ name: "ShikiWorker" },
);
await client.init();
const themeUrl = initThemeUrl || themeUrls[0];
await loadLanguages();
await client.run("setOnigasm", { wasm: shikiOnigasmSrc });
await client.run("setHighlighter", { theme: themeUrl, langs: [] });
shiki.loadedThemes.add(themeUrl);
await shiki._setTheme(themeUrl);
resolveClient(client);
},
_setTheme: async (themeUrl: string) => {
shiki.currentThemeUrl = themeUrl;
const { themeData } = await shiki.client!.run("getTheme", { theme: themeUrl });
shiki.currentTheme = JSON.parse(themeData);
dispatchTheme({ id: themeUrl, theme: shiki.currentTheme });
},
loadTheme: async (themeUrl: string) => {
const client = await shiki.clientPromise;
if (shiki.loadedThemes.has(themeUrl)) return;
await client.run("loadTheme", { theme: themeUrl });
shiki.loadedThemes.add(themeUrl);
},
setTheme: async (themeUrl: string) => {
await shiki.clientPromise;
themeUrl ||= themeUrls[0];
if (!shiki.loadedThemes.has(themeUrl)) await shiki.loadTheme(themeUrl);
await shiki._setTheme(themeUrl);
},
loadLang: async (langId: string) => {
const client = await shiki.clientPromise;
const lang = resolveLang(langId);
if (!lang || shiki.loadedLangs.has(lang.id)) return;
await client.run("loadLanguage", {
lang: {
...lang,
grammar: lang.grammar ?? await getGrammar(lang),
}
});
shiki.loadedLangs.add(lang.id);
},
tokenizeCode: async (code: string, langId: string): Promise<IThemedToken[][]> => {
const client = await shiki.clientPromise;
const lang = resolveLang(langId);
if (!lang) return [];
if (!shiki.loadedLangs.has(lang.id)) await shiki.loadLang(lang.id);
return await client.run("codeToThemedTokens", {
code,
lang: langId,
theme: shiki.currentThemeUrl ?? themeUrls[0],
});
},
destroy() {
shiki.currentTheme = null;
shiki.currentThemeUrl = null;
dispatchTheme({ id: null, theme: null });
shiki.client?.destroy();
}
};

View file

@ -0,0 +1,67 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { IShikiTheme } from "@vap/shiki";
export const SHIKI_REPO = "shikijs/shiki";
export const SHIKI_REPO_COMMIT = "0b28ad8ccfbf2615f2d9d38ea8255416b8ac3043";
export const shikiRepoTheme = (name: string) => `https://raw.githubusercontent.com/${SHIKI_REPO}/${SHIKI_REPO_COMMIT}/packages/shiki/themes/${name}.json`;
export const themes = {
// Default
DarkPlus: shikiRepoTheme("dark-plus"),
// Dev Choices
MaterialCandy: "https://raw.githubusercontent.com/millsp/material-candy/master/material-candy.json",
// More from Shiki repo
DraculaSoft: shikiRepoTheme("dracula-soft"),
Dracula: shikiRepoTheme("dracula"),
GithubDarkDimmed: shikiRepoTheme("github-dark-dimmed"),
GithubDark: shikiRepoTheme("github-dark"),
GithubLight: shikiRepoTheme("github-light"),
LightPlus: shikiRepoTheme("light-plus"),
MaterialDarker: shikiRepoTheme("material-darker"),
MaterialDefault: shikiRepoTheme("material-default"),
MaterialLighter: shikiRepoTheme("material-lighter"),
MaterialOcean: shikiRepoTheme("material-ocean"),
MaterialPalenight: shikiRepoTheme("material-palenight"),
MinDark: shikiRepoTheme("min-dark"),
MinLight: shikiRepoTheme("min-light"),
Monokai: shikiRepoTheme("monokai"),
Nord: shikiRepoTheme("nord"),
OneDarkPro: shikiRepoTheme("one-dark-pro"),
Poimandres: shikiRepoTheme("poimandres"),
RosePineDawn: shikiRepoTheme("rose-pine-dawn"),
RosePineMoon: shikiRepoTheme("rose-pine-moon"),
RosePine: shikiRepoTheme("rose-pine"),
SlackDark: shikiRepoTheme("slack-dark"),
SlackOchin: shikiRepoTheme("slack-ochin"),
SolarizedDark: shikiRepoTheme("solarized-dark"),
SolarizedLight: shikiRepoTheme("solarized-light"),
VitesseDark: shikiRepoTheme("vitesse-dark"),
VitesseLight: shikiRepoTheme("vitesse-light"),
CssVariables: shikiRepoTheme("css-variables"),
};
export const themeCache = new Map<string, IShikiTheme>();
export const getTheme = (url: string): Promise<IShikiTheme> => {
if (themeCache.has(url)) return Promise.resolve(themeCache.get(url)!);
return fetch(url).then(res => res.json());
};

View file

@ -0,0 +1,46 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Clipboard } from "@webpack/common";
import { cl } from "../utils/misc";
import { CopyButton } from "./CopyButton";
export interface ButtonRowProps {
theme: import("./Highlighter").ThemeBase;
content: string;
}
export function ButtonRow({ content, theme }: ButtonRowProps) {
const buttons: JSX.Element[] = [];
if (Clipboard.SUPPORTS_COPY) {
buttons.push(
<CopyButton
content={content}
className={cl("btn")}
style={{
backgroundColor: theme.accentBgColor,
color: theme.accentFgColor,
}}
/>
);
}
return <div className={cl("btns")}>{buttons}</div>;
}

View file

@ -0,0 +1,93 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { IThemedToken } from "@vap/shiki";
import { hljs } from "@webpack/common";
import { cl } from "../utils/misc";
import { ThemeBase } from "./Highlighter";
export interface CodeProps {
theme: ThemeBase;
useHljs: boolean;
lang?: string;
content: string;
tokens: IThemedToken[][] | null;
}
export const Code = ({
theme,
useHljs,
lang,
content,
tokens,
}: CodeProps) => {
let lines!: JSX.Element[];
if (useHljs) {
try {
const { value: hljsHtml } = hljs.highlight(lang!, content, true);
lines = hljsHtml
.split("\n")
.map((line, i) => <span key={i} dangerouslySetInnerHTML={{ __html: line }} />);
} catch {
lines = content.split("\n").map(line => <span>{line}</span>);
}
} else {
const renderTokens =
tokens ??
content
.split("\n")
.map(line => [{ color: theme.plainColor, content: line } as IThemedToken]);
lines = renderTokens.map(line => {
// [Cynthia] this makes it so when you highlight the codeblock
// empty lines are also selected and copied when you Ctrl+C.
if (line.length === 0) {
return <span>{"\n"}</span>;
}
return (
<>
{line.map(({ content, color, fontStyle }, i) => (
<span
key={i}
style={{
color,
fontStyle: (fontStyle ?? 0) & 1 ? "italic" : undefined,
fontWeight: (fontStyle ?? 0) & 2 ? "bold" : undefined,
textDecoration: (fontStyle ?? 0) & 4 ? "underline" : undefined,
}}
>
{content}
</span>
))}
</>
);
});
}
const codeTableRows = lines.map((line, i) => (
<tr key={i}>
<td style={{ color: theme.plainColor }}>{i + 1}</td>
<td>{line}</td>
</tr>
));
return <table className={cl("table")}>{...codeTableRows}</table>;
};

View file

@ -0,0 +1,41 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useCopyCooldown } from "../hooks/useCopyCooldown";
export interface CopyButtonProps extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
content: string;
}
export function CopyButton({ content, ...props }: CopyButtonProps) {
const [copyCooldown, copy] = useCopyCooldown(1000);
return (
<button
{...props}
style={{
...props.style,
cursor: copyCooldown ? "default" : undefined,
}}
onClick={() => copy(content)}
>
{copyCooldown ? "Copied!" : "Copy"}
</button>
);
}

View file

@ -0,0 +1,42 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Language } from "../api/languages";
import { DeviconSetting } from "../types";
import { cl } from "../utils/misc";
export interface HeaderProps {
langName?: string;
useDevIcon: DeviconSetting;
shikiLang: Language | null;
}
export function Header({ langName, useDevIcon, shikiLang }: HeaderProps) {
if (!langName) return <></>;
return (
<div className={cl("lang")}>
{useDevIcon !== DeviconSetting.Disabled && shikiLang?.devicon && (
<i
className={`devicon-${shikiLang.devicon}${useDevIcon === DeviconSetting.Color ? " colored" : ""}`}
/>
)}
{langName}
</div>
);
}

View file

@ -0,0 +1,131 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { useAwaiter } from "@utils/misc";
import { useIntersection } from "@utils/react";
import { hljs, React } from "@webpack/common";
import { resolveLang } from "../api/languages";
import { shiki } from "../api/shiki";
import { useShikiSettings } from "../hooks/useShikiSettings";
import { useTheme } from "../hooks/useTheme";
import { hex2Rgb } from "../utils/color";
import { cl, shouldUseHljs } from "../utils/misc";
import { ButtonRow } from "./ButtonRow";
import { Code } from "./Code";
import { Header } from "./Header";
export interface ThemeBase {
plainColor: string;
accentBgColor: string;
accentFgColor: string;
backgroundColor: string;
}
export interface HighlighterProps {
lang?: string;
content: string;
isPreview: boolean;
tempSettings?: Record<string, any>;
}
export const createHighlighter = (props: HighlighterProps) => (
<pre className={cl("container")}>
<ErrorBoundary>
<Highlighter {...props} />
</ErrorBoundary>
</pre>
);
export const Highlighter = ({
lang,
content,
isPreview,
tempSettings,
}: HighlighterProps) => {
const {
tryHljs,
useDevIcon,
bgOpacity,
} = useShikiSettings(["tryHljs", "useDevIcon", "bgOpacity"], tempSettings);
const { id: currentThemeId, theme: currentTheme } = useTheme();
const shikiLang = lang ? resolveLang(lang) : null;
const useHljs = shouldUseHljs({ lang, tryHljs });
const [rootRef, isIntersecting] = useIntersection(true);
const [tokens] = useAwaiter(async () => {
if (!shikiLang || useHljs || !isIntersecting) return null;
return await shiki.tokenizeCode(content, lang!);
}, {
fallbackValue: null,
deps: [lang, content, currentThemeId, isIntersecting],
});
const themeBase: ThemeBase = {
plainColor: currentTheme?.fg || "var(--text-normal)",
accentBgColor:
currentTheme?.colors?.["statusBar.background"] || (useHljs ? "#7289da" : "#007BC8"),
accentFgColor: currentTheme?.colors?.["statusBar.foreground"] || "#FFF",
backgroundColor:
currentTheme?.colors?.["editor.background"] || "var(--background-secondary)",
};
let langName;
if (lang) langName = useHljs ? hljs?.getLanguage?.(lang)?.name : shikiLang?.name;
const preClasses = [cl("root")];
if (!langName) preClasses.push(cl("plain"));
if (isPreview) preClasses.push(cl("preview"));
return (
<div
ref={rootRef}
className={preClasses.join(" ")}
style={{
backgroundColor: useHljs
? themeBase.backgroundColor
: `rgba(${hex2Rgb(themeBase.backgroundColor)
.concat(bgOpacity / 100)
.join(", ")})`,
color: themeBase.plainColor,
}}
>
<code>
<Header
langName={langName}
useDevIcon={useDevIcon}
shikiLang={shikiLang}
/>
<Code
theme={themeBase}
useHljs={useHljs}
lang={lang}
content={content}
tokens={tokens}
/>
{!isPreview && <ButtonRow
content={content}
theme={themeBase}
/>}
</code>
</div>
);
};

View file

@ -0,0 +1,34 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Clipboard, React } from "@webpack/common";
export function useCopyCooldown(cooldown: number) {
const [copyCooldown, setCopyCooldown] = React.useState(false);
function copy(text: string) {
Clipboard.copy(text);
setCopyCooldown(true);
setTimeout(() => {
setCopyCooldown(false);
}, cooldown);
}
return [copyCooldown, copy] as const;
}

View file

@ -0,0 +1,47 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useSettings } from "@api/settings";
import { React } from "@webpack/common";
import { shiki } from "../api/shiki";
import { ShikiSettings } from "../types";
export function useShikiSettings(settingKeys: (keyof ShikiSettings)[], overrides?: Record<string, any>) {
const settings = useSettings(settingKeys.map(key => `plugins.ShikiCodeblocks.${key}`)).plugins.ShikiCodeblocks as ShikiSettings;
const [isLoading, setLoading] = React.useState(false);
const withOverrides = { ...settings, ...overrides };
const themeUrl = withOverrides.customTheme || withOverrides.theme;
if (overrides) {
const willChangeTheme = shiki.currentThemeUrl && themeUrl !== shiki.currentThemeUrl;
const noOverrides = Object.keys(overrides).length === 0;
if (isLoading && (!willChangeTheme || noOverrides)) setLoading(false);
if ((!isLoading && willChangeTheme)) {
setLoading(true);
shiki.setTheme(themeUrl);
}
}
return {
...withOverrides,
isThemeLoading: themeUrl !== shiki.currentThemeUrl,
};
}

View file

@ -0,0 +1,49 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { React } from "@webpack/common";
type Shiki = typeof import("../api/shiki").shiki;
interface ThemeState {
id: Shiki["currentThemeUrl"],
theme: Shiki["currentTheme"],
}
const currentTheme: ThemeState = {
id: null,
theme: null,
};
const themeSetters = new Set<React.Dispatch<React.SetStateAction<ThemeState>>>();
export const useTheme = (): ThemeState => {
const [, setTheme] = React.useState<ThemeState>(currentTheme);
React.useEffect(() => {
themeSetters.add(setTheme);
return () => void themeSetters.delete(setTheme);
}, []);
return currentTheme;
};
export function dispatchTheme(state: ThemeState) {
if (currentTheme.id === state.id) return;
Object.assign(currentTheme, state);
themeSetters.forEach(setTheme => setTheme(state));
}

View file

@ -0,0 +1,164 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import { parseUrl } from "@utils/misc";
import { wordsFromPascal, wordsToTitle } from "@utils/text";
import definePlugin, { OptionType } from "@utils/types";
import previewExampleText from "~fileContent/previewExample.tsx";
import cssText from "~fileContent/style.css";
import { Settings } from "../../Vencord";
import { shiki } from "./api/shiki";
import { themes } from "./api/themes";
import { createHighlighter } from "./components/Highlighter";
import { DeviconSetting, HljsSetting, ShikiSettings, StyleSheets } from "./types";
import { clearStyles, removeStyle, setStyle } from "./utils/createStyle";
const themeNames = Object.keys(themes);
const devIconCss = "@import url('https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css');";
const getSettings = () => Settings.plugins.ShikiCodeblocks as ShikiSettings;
export default definePlugin({
name: "ShikiCodeblocks",
description: "Brings vscode-style codeblocks into Discord, powered by Shiki",
authors: [Devs.Vap],
patches: [
{
find: "codeBlock:{react:function",
replacement: {
match: /codeBlock:\{react:function\((.),(.),(.)\)\{/,
replace: "$&return Vencord.Plugins.plugins.ShikiCodeblocks.renderHighlighter($1,$2,$3);",
},
},
],
start: async () => {
setStyle(cssText, StyleSheets.Main);
if (getSettings().useDevIcon !== DeviconSetting.Disabled)
setStyle(devIconCss, StyleSheets.DevIcons);
await shiki.init(getSettings().customTheme || getSettings().theme);
},
stop: () => {
shiki.destroy();
clearStyles();
},
settingsAboutComponent: ({ tempSettings }) => createHighlighter({
lang: "tsx",
content: previewExampleText,
isPreview: true,
tempSettings,
}),
options: {
theme: {
type: OptionType.SELECT,
description: "Default themes",
options: themeNames.map(themeName => ({
label: wordsToTitle(wordsFromPascal(themeName)),
value: themes[themeName],
default: themes[themeName] === themes.DarkPlus,
})),
disabled: () => !!getSettings().customTheme,
onChange: shiki.setTheme,
},
customTheme: {
type: OptionType.STRING,
description: "A link to a custom vscode theme",
placeholder: themes.MaterialCandy,
isValid: value => {
if (!value) return true;
const url = parseUrl(value);
if (!url) return "Must be a valid URL";
if (!url.pathname.endsWith(".json")) return "Must be a json file";
return true;
},
onChange: value => shiki.setTheme(value || getSettings().theme),
},
tryHljs: {
type: OptionType.SELECT,
description: "Use the more lightweight default Discord highlighter and theme.",
options: [
{
label: "Never",
value: HljsSetting.Never,
},
{
label: "Prefer Shiki instead of Highlight.js",
value: HljsSetting.Secondary,
default: true,
},
{
label: "Prefer Highlight.js instead of Shiki",
value: HljsSetting.Primary,
},
{
label: "Always",
value: HljsSetting.Always,
},
],
},
useDevIcon: {
type: OptionType.SELECT,
description: "How to show language icons on codeblocks",
options: [
{
label: "Disabled",
value: DeviconSetting.Disabled,
},
{
label: "Colorless",
value: DeviconSetting.Greyscale,
default: true,
},
{
label: "Colored",
value: DeviconSetting.Color,
},
],
onChange: (newValue: DeviconSetting) => {
if (newValue === DeviconSetting.Disabled) removeStyle(StyleSheets.DevIcons);
else setStyle(devIconCss, StyleSheets.DevIcons);
},
},
bgOpacity: {
type: OptionType.SLIDER,
description: "Background opacity",
markers: [0, 20, 40, 60, 80, 100],
default: 100,
componentProps: {
stickToMarkers: false,
onValueRender: null, // Defaults to percentage
},
},
},
// exports
shiki,
createHighlighter,
renderHighlighter: ({ lang, content }: { lang: string; content: string; }) => {
return createHighlighter({
lang,
content,
isPreview: false,
});
},
});

View file

@ -0,0 +1,13 @@
/* eslint-disable header/header */
import React from "react";
const handleClick = async () =>
console.log((await import("@webpack/common")).Clipboard.copy("\u200b"));
export const Example: React.FC<{
real: boolean,
shigged?: number,
}> = ({ real, shigged }) => <>
<p>{`Shigg${real ? `ies${shigged === 0x1B ? "t" : ""}` : "y"}`}</p>
<button onClick={handleClick}>Click Me</button>
</>;

View file

@ -0,0 +1,103 @@
.shiki-root {
border-radius: 4px;
/* fallback background */
background-color: var(--background-secondary);
}
.shiki-root code {
display: block;
overflow-x: auto;
padding: 0.5em;
position: relative;
font-size: 0.875rem;
line-height: 1.125rem;
text-indent: 0;
white-space: pre-wrap;
background: transparent;
border: none;
}
.shiki-root [class^='devicon-'],
.shiki-root [class*=' devicon-'] {
margin-right: 8px;
user-select: none;
}
.shiki-plain code {
padding-top: 8px;
}
.shiki-btns {
font-size: 1em;
position: absolute;
right: 0;
bottom: 0;
opacity: 0;
}
.shiki-root:hover .shiki-btns {
opacity: 1;
}
.shiki-btn {
border-radius: 4px 4px 0 0;
padding: 4px 8px;
}
.shiki-btn~.shiki-btn {
margin-left: 4px;
}
.shiki-btn:last-child {
border-radius: 4px 0;
}
.shiki-spinner-container {
align-items: center;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
position: absolute;
justify-content: center;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.shiki-preview {
margin-bottom: 2em;
}
.shiki-lang {
padding: 0 5px;
margin-bottom: 6px;
font-weight: bold;
text-transform: capitalize;
display: flex;
align-items: center;
}
.shiki-table {
border-collapse: collapse;
width: 100%;
}
.shiki-table tr {
height: 19px;
width: 100%;
}
.shiki-root td:first-child {
border-right: 1px solid transparent;
padding-left: 5px;
padding-right: 8px;
user-select: none;
}
.shiki-root td:last-child {
padding-left: 8px;
word-break: break-word;
width: 100%;
}

View file

@ -0,0 +1,78 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type {
ILanguageRegistration,
IShikiTheme,
IThemedToken,
IThemeRegistration,
} from "@vap/shiki";
import type { Settings } from "../../Vencord";
/** This must be atleast a subset of the `@vap/shiki-worker` spec */
export type ShikiSpec = {
setOnigasm: ({ wasm }: { wasm: string; }) => Promise<void>;
setHighlighter: ({ theme, langs }: {
theme: IThemeRegistration | void;
langs: ILanguageRegistration[];
}) => Promise<void>;
loadTheme: ({ theme }: {
theme: string | IShikiTheme;
}) => Promise<void>;
getTheme: ({ theme }: { theme: string; }) => Promise<{ themeData: string; }>;
loadLanguage: ({ lang }: { lang: ILanguageRegistration; }) => Promise<void>;
codeToThemedTokens: ({
code,
lang,
theme,
}: {
code: string;
lang?: string;
theme?: string;
}) => Promise<IThemedToken[][]>;
};
export enum StyleSheets {
Main = "MAIN",
DevIcons = "DEVICONS",
}
export enum HljsSetting {
Never = "NEVER",
Secondary = "SECONDARY",
Primary = "PRIMARY",
Always = "ALWAYS",
}
export enum DeviconSetting {
Disabled = "DISABLED",
Greyscale = "GREYSCALE",
Color = "COLOR"
}
type CommonSettings = {
[K in keyof Settings["plugins"][string]as K extends `${infer V}` ? K : never]: Settings["plugins"][string][K];
};
export interface ShikiSettings extends CommonSettings {
theme: string;
customTheme: string;
tryHljs: HljsSetting;
useDevIcon: DeviconSetting;
bgOpacity: number;
}

View file

@ -0,0 +1,32 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export function hex2Rgb(hex: string) {
hex = hex.slice(1);
if (hex.length < 6)
hex = hex
.split("")
.map(c => c + c)
.join("");
if (hex.length === 6) hex += "ff";
if (hex.length > 6) hex = hex.slice(0, 6);
return hex
.split(/(..)/)
.filter(Boolean)
.map(c => parseInt(c, 16));
}

View file

@ -0,0 +1,36 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const styles = new Map<string, HTMLStyleElement>();
export function setStyle(css: string, id: string) {
const style = document.createElement("style");
style.innerText = css;
document.head.appendChild(style);
styles.set(id, style);
}
export function removeStyle(id: string) {
styles.get(id)?.remove();
return styles.delete(id);
}
export const clearStyles = () => {
styles.forEach(style => style.remove());
styles.clear();
};

View file

@ -0,0 +1,50 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { hljs } from "@webpack/common";
import { resolveLang } from "../api/languages";
import { HighlighterProps } from "../components/Highlighter";
import { HljsSetting, ShikiSettings } from "../types";
export const cl = (className: string) => `shiki-${className}`;
export const shouldUseHljs = ({
lang,
tryHljs,
}: {
lang: HighlighterProps["lang"],
tryHljs: ShikiSettings["tryHljs"],
}) => {
const hljsLang = lang ? hljs?.getLanguage?.(lang) : null;
const shikiLang = lang ? resolveLang(lang) : null;
const langName = shikiLang?.name;
switch (tryHljs) {
case HljsSetting.Always:
return true;
case HljsSetting.Primary:
return !!hljsLang || lang === "";
case HljsSetting.Secondary:
return !langName && !!hljsLang;
case HljsSetting.Never:
return false;
}
return false;
};

View file

@ -0,0 +1,74 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { RelationshipStore } from "@webpack/common";
import { User } from "discord-types/general";
import { Settings } from "Vencord";
export default definePlugin({
name: "SortFriendRequests",
authors: [Devs.Megu],
description: "Sorts friend requests by date of receipt",
patches: [{
find: ".PENDING_INCOMING||",
replacement: [{
match: /\.sortBy\(\(function\((\w)\){return \w{1,3}\.comparator}\)\)/,
// If the row type is 3 or 4 (pendinng incoming or outgoing), sort by date of receipt
// Otherwise, use the default comparator
replace: (_, row) => `.sortBy((function(${row}) {
return ${row}.type === 3 || ${row}.type === 4
? -Vencord.Plugins.plugins.SortFriendRequests.getSince(${row}.user)
: ${row}.comparator
}))`
}, {
predicate: () => Settings.plugins.SortFriendRequests.showDates,
match: /(user:(\w{1,3}),.{10,30}),subText:(\w{1,3}),(.{10,30}userInfo}\))/,
// Show dates in the friend request list
replace: (_, pre, user, subText, post) => `${pre},
subText: Vencord.Plugins.plugins.SortFriendRequests.makeSubtext(${subText}, ${user}),
${post}`
}]
}],
getSince(user: User) {
return new Date(RelationshipStore.getSince(user.id));
},
makeSubtext(text: string, user: User) {
const since = this.getSince(user);
return (
<Flex flexDirection="row" style={{ gap: 0, flexWrap: "wrap", lineHeight: "0.9rem" }}>
<span>{text}</span>
{!isNaN(since.getTime()) && <span>Received &mdash; {since.toDateString()}</span>}
</Flex>
);
},
options: {
showDates: {
type: OptionType.BOOLEAN,
description: "Show dates on friend requests",
default: false,
restartNeeded: true
}
}
});

147
src/plugins/viewRaw.tsx Normal file
View file

@ -0,0 +1,147 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addButton, removeButton } from "@api/MessagePopover";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import { copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Button, ChannelStore, Forms, Margins, Parser } from "@webpack/common";
import { Message } from "discord-types/general";
const CopyIcon = () => {
return <svg viewBox="0 0 512.002 512.002" fill="currentColor" aria-hidden="true" width="22" height="22">
<path d="M462.002,92.002h-42.001V50c0-27.57-22.43-50-50-50h-320c-27.57,0-50,22.43-50,50v320.002c0,27.57,22.43,50,50,50h42.001 v42c0,27.57,22.43,50,50,50h320c27.57,0,50-22.43,50-50v-320C512.001,114.432,489.573,92.002,462.002,92.002z M50.001,400.002 c-16.542,0-30-13.458-30-30V50c0-16.542,13.458-30,30-30h320c16.542,0,30,13.458,30,30v320.002c0,16.542-13.458,30-30,30H50.001z M492.002,462.002c0,16.542-13.458,30-30,30h-320c-16.542,0-30-13.458-30-30v-42h257.999c27.57,0,50-22.43,50-50v-258h42.001 c16.542,0,30,13.458,30,30V462.002z" />
<path d="M462.024,457.002H170.98c-5.522,0-10,4.478-10,10c0,5.523,4.478,10,10,10h291.043c5.522,0,10-4.477,10-10 S467.546,457.002,462.024,457.002z" />
<path d="M142.25,457.002h-0.27c-5.522,0-10,4.478-10,10c0,5.523,4.478,10,10,10h0.27c5.523,0,10-4.477,10-10 S147.773,457.002,142.25,457.002z" />
<path d="M110.035,35h-0.27c-5.522,0-10,4.478-10,10s4.478,10,10,10h0.27c5.522,0,10-4.478,10-10S115.558,35,110.035,35z" />
<path d="M81.036,35H50.001c-5.522,0-10,4.478-10,10s4.478,10,10,10h31.034c5.523,0,10.001-4.478,10.001-10S86.558,35,81.036,35z" />
<path d="M122.084,246.829l-0.008-0.008l-14.8-30.002c5.407-2.305,10.07-5.975,13.466-10.537c3.725-5.006,5.931-11.08,5.931-17.604 c0-8.588-3.248-15.902-9.743-21.965c-3.196-2.992-6.739-5.242-10.627-6.744c-3.891-1.506-8.097-2.258-12.612-2.258H64.936 c-2.793,0-5.327,1.145-7.163,2.996c-1.832,1.85-2.968,4.396-2.968,7.205v82.777c0,2.896,1.137,5.338,2.893,7.105 c0.962,0.969,2.107,1.728,3.349,2.246c1.249,0.52,2.597,0.797,3.953,0.797c2.49,0,5.028-0.92,7.096-2.967 c1.004-0.99,1.761-2.09,2.271-3.297c0.516-1.221,0.771-2.516,0.771-3.885v-31.139h10.844l17.862,36.184l-0.004,0.005 c1.315,2.699,3.416,4.4,5.763,5.201c1.275,0.434,2.616,0.596,3.938,0.502c1.317-0.094,2.618-0.44,3.812-1.022 c2.307-1.123,4.237-3.123,5.179-5.881h0.002c0.45-1.307,0.639-2.609,0.564-3.904C123.023,249.327,122.684,248.058,122.084,246.829 z M106.124,190.85c-0.438,1.795-1.473,3.546-3.189,5.07c-1.205,1.039-2.538,1.838-3.996,2.395c-1.45,0.553-3.02,0.865-4.704,0.934 v-0.006H75.138v-21.221H93.69c3.19,0,5.9,0.893,7.984,2.328c1.83,1.26,3.187,2.939,3.96,4.791 C106.393,186.957,106.591,188.942,106.124,190.85z" />
<path d="M364.199,163.706c-1.12-2.354-3.136-4.285-5.777-5.254l-0.055-0.016c-2.721-0.916-5.49-0.688-7.823,0.439 c-2.349,1.135-4.243,3.172-5.193,5.861v0.008l-19.568,55.744l-19.571-55.816h-0.001c-0.705-2.352-2.14-4.141-3.935-5.322 c-1.589-1.047-3.458-1.609-5.345-1.656c-1.887-0.047-3.801,0.42-5.479,1.43c-1.938,1.164-3.553,3.031-4.445,5.637h0.002 l-19.564,55.73l-19.569-55.746l-0.008-0.016c-0.997-2.779-2.938-4.752-5.235-5.846c-1.222-0.584-2.552-0.918-3.899-0.99 c-1.34-0.074-2.701,0.115-3.991,0.578c-2.347,0.842-4.429,2.57-5.701,5.24c-1.206,2.529-1.354,5.119-0.441,7.766l0.005,0.009 l29.181,83.133l0.016,0.047c1.186,3.139,3.318,5.213,5.775,6.234c1.141,0.473,2.355,0.717,3.581,0.73 c1.219,0.016,2.438-0.197,3.595-0.635c2.602-0.982,4.914-3.084,6.238-6.268l0.039-0.109l19.595-55.779l20.043,56.877l0.047,0.125 c0.967,2.25,2.404,3.912,4.071,4.99c1.666,1.078,3.544,1.563,5.408,1.465c1.854-0.096,3.686-0.764,5.274-1.992 c1.666-1.289,3.059-3.191,3.899-5.686l29.251-83.133C365.546,168.825,365.317,166.053,364.199,163.706z" />
<path d="M226.7,247.37l-0.008-0.031l-8.818-21.24l-0.003,0.002l-25.528-61.617h0.001c-1.024-2.709-3.167-4.654-5.674-5.645 c-1.165-0.461-2.429-0.715-3.708-0.742c-1.28-0.027-2.565,0.178-3.773,0.635c-2.393,0.906-4.479,2.764-5.647,5.719l0.005,0.002 l-25.54,61.719h-0.003l-8.818,21.168v0.008c-1.076,2.607-1,5.383-0.008,7.783c0.994,2.408,2.91,4.433,5.51,5.527l0.038,0.023 c2.619,1.035,5.435,1.064,7.886,0.041c2.295-0.955,4.242-2.813,5.384-5.6l6.147-14.877h37.566l6.148,14.877 c1.175,2.844,3.171,4.674,5.459,5.609c1.261,0.518,2.601,0.752,3.932,0.73c1.324-0.021,2.642-0.295,3.865-0.801 c2.418-0.998,4.496-2.9,5.579-5.547l0.008-0.016C227.723,252.523,227.723,249.945,226.7,247.37z M172.552,219.938l10.344-25.01 l10.416,25.01H172.552z" />
</svg>;
};
function sortObject<T extends object>(obj: T): T {
return Object.fromEntries(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))) as T;
}
function cleanMessage(msg: Message) {
const clone = sortObject(JSON.parse(JSON.stringify(msg)));
for (const key in clone.author) {
switch (key) {
case "id":
case "username":
case "usernameNormalized":
case "discriminator":
case "avatar":
case "bot":
case "system":
case "publicFlags":
break;
default:
// phone number, email, etc
delete clone.author[key];
}
}
// message logger added properties
const cloneAny = clone as any;
delete cloneAny.editHistory;
delete cloneAny.deleted;
cloneAny.attachments?.forEach(a => delete a.deleted);
return clone;
}
function CodeBlock(props: { content: string, lang: string; }) {
return (
// make text selectable
<div style={{ userSelect: "text" }}>
{Parser.defaultRules.codeBlock.react(props, null, {})}
</div>
);
}
function openViewRawModal(msg: Message) {
msg = cleanMessage(msg);
const msgJson = JSON.stringify(msg, null, 4);
const key = openModal(props => (
<ErrorBoundary>
<ModalRoot {...props} size={ModalSize.LARGE}>
<ModalHeader>
<Forms.FormTitle tag="h1">View Raw</Forms.FormTitle>
<ModalCloseButton onClick={() => closeModal(key)} />
</ModalHeader>
<ModalContent style={{ padding: "1em" }}>
<Flex style={{ marginBottom: "1em", marginTop: "1em" }}>
<Button onClick={() => copyWithToast(msg.content, "Content copied to clipboard!")}>
Copy Raw Content
</Button>
<Button onClick={() => copyWithToast(msgJson, "Message data copied to clipboard!")}>
Copy Message JSON
</Button>
</Flex>
{!!msg.content && (
<>
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
<CodeBlock content={msg.content} lang="" />
<Forms.FormDivider classes={Margins.marginBottom20} />
</>
)}
<Forms.FormTitle tag="h5">Message Data</Forms.FormTitle>
<CodeBlock content={msgJson} lang="json" />
</ModalContent >
</ModalRoot >
</ErrorBoundary >
));
}
export default definePlugin({
name: "ViewRaw",
description: "Copy and view the raw content/data of any message.",
authors: [Devs.KingFish, Devs.Ven],
dependencies: ["MessagePopoverAPI"],
start() {
addButton("ViewRaw", msg => {
return {
label: "View Raw (Left Click) / Copy Raw (Right Click)",
icon: CopyIcon,
message: msg,
channel: ChannelStore.getChannel(msg.channel_id),
onClick: () => openViewRawModal(msg),
onContextMenu: e => {
e.preventDefault();
e.stopPropagation();
copyWithToast(msg.content);
}
};
});
},
stop() {
removeButton("CopyRawMessage");
}
});

View file

@ -156,5 +156,17 @@ export const Devs = Object.freeze({
Luna: { Luna: {
name: "Luny", name: "Luny",
id: 821472922140803112n id: 821472922140803112n
} },
Vap: {
name: "Vap0r1ze",
id: 454072114492866560n
},
KingFish: {
name: "King Fish",
id: 499400512559382538n
},
Commandtechno: {
name: "Commandtechno",
id: 296776625432035328n,
},
}); });

View file

@ -74,3 +74,7 @@ export interface ApngFrameData {
frames: ApngFrame[]; frames: ApngFrame[];
playTime: number; playTime: number;
} }
const shikiWorkerDist = "https://unpkg.com/@vap/shiki-worker@0.0.8/dist";
export const shikiWorkerSrc = `${shikiWorkerDist}/${IS_DEV ? "index.js" : "index.min.js"}`;
export const shikiOnigasmSrc = "https://unpkg.com/@vap/shiki@0.10.3/dist/onig.wasm";

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { React } from "@webpack/common"; import { Clipboard, React, Toasts } from "@webpack/common";
/** /**
* Makes a lazy function. On first call, the value is computed. * Makes a lazy function. On first call, the value is computed.
@ -28,7 +28,12 @@ export function makeLazy<T>(factory: () => T): () => T {
return () => cache ?? (cache = factory()); return () => cache ?? (cache = factory());
} }
type AwaiterRes<T> = [T, any, boolean, () => void]; type AwaiterRes<T> = [T, any, boolean];
interface AwaiterOpts<T> {
fallbackValue: T,
deps?: unknown[],
onError?(e: any): void,
}
/** /**
* Await a promise * Await a promise
* @param factory Factory * @param factory Factory
@ -36,26 +41,31 @@ type AwaiterRes<T> = [T, any, boolean, () => void];
* @returns [value, error, isPending] * @returns [value, error, isPending]
*/ */
export function useAwaiter<T>(factory: () => Promise<T>): AwaiterRes<T | null>; export function useAwaiter<T>(factory: () => Promise<T>): AwaiterRes<T | null>;
export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: T): AwaiterRes<T>; export function useAwaiter<T>(factory: () => Promise<T>, providedOpts: AwaiterOpts<T>): AwaiterRes<T>;
export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: null, onError: (e: unknown) => unknown): AwaiterRes<T>; export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterOpts<T | null>): AwaiterRes<T | null> {
export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: T | null = null, onError?: (e: unknown) => unknown): AwaiterRes<T | null> { const opts: Required<AwaiterOpts<T | null>> = Object.assign({
fallbackValue: null,
deps: [],
onError: null,
}, providedOpts);
const [state, setState] = React.useState({ const [state, setState] = React.useState({
value: fallbackValue, value: opts.fallbackValue,
error: null, error: null,
pending: true pending: true
}); });
const [signal, setSignal] = React.useState(0);
React.useEffect(() => { React.useEffect(() => {
let isAlive = true; let isAlive = true;
if (!state.pending) setState({ ...state, pending: true });
factory() factory()
.then(value => isAlive && setState({ value, error: null, pending: false })) .then(value => isAlive && setState({ value, error: null, pending: false }))
.catch(error => isAlive && (setState({ value: null, error, pending: false }), onError?.(error))); .catch(error => isAlive && (setState({ value: null, error, pending: false }), opts.onError?.(error)));
return () => void (isAlive = false); return () => void (isAlive = false);
}, [signal]); }, opts.deps);
return [state.value, state.error, state.pending, () => setSignal(signal + 1)]; return [state.value, state.error, state.pending];
} }
/** /**
@ -175,5 +185,46 @@ export function suppressErrors<F extends Function>(name: string, func: F, thisOb
*/ */
export function makeCodeblock(text: string, language?: string) { export function makeCodeblock(text: string, language?: string) {
const chars = "```"; const chars = "```";
return `${chars}${language || ""}\n${text}\n${chars}`; return `${chars}${language || ""}\n${text.replaceAll("```", "\\`\\`\\`")}\n${chars}`;
} }
export function copyWithToast(text: string, toastMessage = "Copied to clipboard!") {
if (Clipboard.SUPPORTS_COPY) {
Clipboard.copy(text);
} else {
toastMessage = "Your browser does not support copying to clipboard";
}
Toasts.show({
message: toastMessage,
id: Toasts.genId(),
type: Toasts.Type.SUCCESS
});
}
/**
* Check if obj is a true object: of type "object" and not null or array
*/
export function isObject(obj: unknown): obj is object {
return typeof obj === "object" && obj !== null && !Array.isArray(obj);
}
/**
* Returns null if value is not a URL, otherwise return URL object.
* Avoids having to wrap url checks in a try/catch
*/
export function parseUrl(urlString: string): URL | null {
try {
return new URL(urlString);
} catch {
return null;
}
}
/**
* Checks whether an element is on screen
*/
export const checkIntersecting = (el: Element) => {
const elementBox = el.getBoundingClientRect();
const documentHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
return !(elementBox.bottom < 0 || elementBox.top - documentHeight >= 0);
};

62
src/utils/react.ts Normal file
View file

@ -0,0 +1,62 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { React } from "@webpack/common";
import { checkIntersecting } from "./misc";
/**
* Check if an element is on screen
* @param intersectOnly If `true`, will only update the state when the element comes into view
* @returns [refCallback, isIntersecting]
*/
export const useIntersection = (intersectOnly = false): [
refCallback: React.RefCallback<Element>,
isIntersecting: boolean,
] => {
const observerRef = React.useRef<IntersectionObserver | null>(null);
const [isIntersecting, setIntersecting] = React.useState(false);
const refCallback = (element: Element | null) => {
observerRef.current?.disconnect();
observerRef.current = null;
if (!element) return;
if (checkIntersecting(element)) {
setIntersecting(true);
if (intersectOnly) return;
}
observerRef.current = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.target !== element) continue;
if (entry.isIntersecting && intersectOnly) {
setIntersecting(true);
observerRef.current?.disconnect();
observerRef.current = null;
} else {
setIntersecting(entry.isIntersecting);
}
}
});
observerRef.current.observe(element);
};
return [refCallback, isIntersecting];
};

36
src/utils/text.ts Normal file
View file

@ -0,0 +1,36 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Utils for readable text transformations eg: `toTitle(fromKebab())`
// Case style to words
export const wordsFromCamel = (text: string) => text.split(/(?=[A-Z])/).map(w => w.toLowerCase());
export const wordsFromSnake = (text: string) => text.toLowerCase().split("_");
export const wordsFromKebab = (text: string) => text.toLowerCase().split("-");
export const wordsFromPascal = (text: string) => text.split(/(?=[A-Z])/).map(w => w.toLowerCase());
export const wordsFromTitle = (text: string) => text.toLowerCase().split(" ");
// Words to case style
export const wordsToCamel = (words: string[]) =>
words.map((w, i) => (i ? w[0].toUpperCase() + w.slice(1) : w)).join("");
export const wordsToSnake = (words: string[]) => words.join("_").toUpperCase();
export const wordsToKebab = (words: string[]) => words.join("-").toLowerCase();
export const wordsToPascal = (words: string[]) =>
words.map(w => w[0].toUpperCase() + w.slice(1)).join("");
export const wordsToTitle = (words: string[]) =>
words.map(w => w[0].toUpperCase() + w.slice(1)).join(" ");

View file

@ -27,6 +27,7 @@ export default function definePlugin<P extends PluginDef>(p: P & Record<string,
export interface PatchReplacement { export interface PatchReplacement {
match: string | RegExp; match: string | RegExp;
replace: string | ((match: string, ...groups: string[]) => string); replace: string | ((match: string, ...groups: string[]) => string);
predicate?(): boolean;
} }
export interface Patch { export interface Patch {
@ -89,7 +90,9 @@ export interface PluginDef {
* Allows you to specify a custom Component that will be rendered in your * Allows you to specify a custom Component that will be rendered in your
* plugin's settings page * plugin's settings page
*/ */
settingsAboutComponent?: React.ComponentType; settingsAboutComponent?: React.ComponentType<{
tempSettings?: Record<string, any>;
}>;
} }
export enum OptionType { export enum OptionType {

View file

@ -37,6 +37,8 @@ export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPorta
export const RestAPI = findByPropsLazy("getAPIBaseURL", "get"); export const RestAPI = findByPropsLazy("getAPIBaseURL", "get");
export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear"); export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear");
export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight");
export const MessageStore = findByPropsLazy("getRawMessages") as Omit<Stores.MessageStore, "getMessages"> & { export const MessageStore = findByPropsLazy("getRawMessages") as Omit<Stores.MessageStore, "getMessages"> & {
getMessages(chanId: string): any; getMessages(chanId: string): any;
}; };
@ -50,7 +52,10 @@ export let UserStore: Stores.UserStore;
export let SelectedChannelStore: Stores.SelectedChannelStore; export let SelectedChannelStore: Stores.SelectedChannelStore;
export let SelectedGuildStore: any; export let SelectedGuildStore: any;
export let ChannelStore: Stores.ChannelStore; export let ChannelStore: Stores.ChannelStore;
export let RelationshipStore: Stores.RelationshipStore; export let RelationshipStore: Stores.RelationshipStore & {
/** Get the date (as a string) that the relationship was created */
getSince(userId: string): string;
};
export const Forms = {} as { export const Forms = {} as {
FormTitle: Components.FormTitle; FormTitle: Components.FormTitle;

View file

@ -137,6 +137,7 @@ function patchPush() {
// @ts-ignore we change all patch.replacement to array in plugins/index // @ts-ignore we change all patch.replacement to array in plugins/index
for (const replacement of patch.replacement) { for (const replacement of patch.replacement) {
if (replacement.predicate && !replacement.predicate()) continue;
const lastMod = mod; const lastMod = mod;
const lastCode = code; const lastCode = code;