mirror of
https://github.com/Vendicated/Vencord.git
synced 2025-02-24 15:35:11 +00:00
Merge branch 'main' into BypassDND
This commit is contained in:
commit
8fdbf00a4f
154 changed files with 7086 additions and 3037 deletions
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
|
@ -18,14 +18,14 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 19
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 19
|
node-version: 20
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|
2
.github/workflows/codeberg-mirror.yml
vendored
2
.github/workflows/codeberg-mirror.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
||||||
if: github.repository == 'Vendicated/Vencord'
|
if: github.repository == 'Vendicated/Vencord'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
|
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
|
||||||
|
|
8
.github/workflows/publish.yml
vendored
8
.github/workflows/publish.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: check that tag matches package.json version
|
- name: check that tag matches package.json version
|
||||||
run: |
|
run: |
|
||||||
|
@ -20,12 +20,12 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 19
|
- name: Use Node.js 19
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 19
|
node-version: 20
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|
23
.github/workflows/reportBrokenPlugins.yml
vendored
23
.github/workflows/reportBrokenPlugins.yml
vendored
|
@ -11,28 +11,31 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
if: ${{ github.event_name == 'schedule' }}
|
if: ${{ github.event_name == 'schedule' }}
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 19
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 19
|
node-version: 20
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
pnpm add puppeteer
|
|
||||||
|
|
||||||
sudo apt-get install -y chromium-browser
|
- name: Install Google Chrome
|
||||||
|
id: setup-chrome
|
||||||
|
uses: browser-actions/setup-chrome@82b9ce628cc5595478a9ebadc480958a36457dc2
|
||||||
|
with:
|
||||||
|
chrome-version: stable
|
||||||
|
|
||||||
- name: Build web
|
- name: Build web
|
||||||
run: pnpm buildWeb --standalone --dev
|
run: pnpm buildWeb --standalone --dev
|
||||||
|
@ -41,7 +44,7 @@ jobs:
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
run: |
|
run: |
|
||||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
export CHROMIUM_BIN=$(which chromium-browser)
|
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
|
||||||
|
|
||||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
esbuild scripts/generateReport.ts > dist/report.mjs
|
||||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||||
|
@ -54,7 +57,7 @@ jobs:
|
||||||
if: success() || failure() # even run if previous one failed
|
if: success() || failure() # even run if previous one failed
|
||||||
run: |
|
run: |
|
||||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
export CHROMIUM_BIN=$(which chromium-browser)
|
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
|
||||||
export USE_CANARY=true
|
export USE_CANARY=true
|
||||||
|
|
||||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
esbuild scripts/generateReport.ts > dist/report.mjs
|
||||||
|
|
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
|
@ -10,13 +10,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 18
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|
1
.npmrc
1
.npmrc
|
@ -1 +1,2 @@
|
||||||
strict-peer-dependencies=false
|
strict-peer-dependencies=false
|
||||||
|
package-manager-strict=false
|
||||||
|
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
@ -1,11 +1,9 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"eamodio.gitlens",
|
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
"ExodiusStudios.comment-anchors",
|
|
||||||
"formulahendry.auto-rename-tag",
|
|
||||||
"GregorBiswanger.json2ts",
|
"GregorBiswanger.json2ts",
|
||||||
"stylelint.vscode-stylelint"
|
"stylelint.vscode-stylelint",
|
||||||
|
"Vendicated.vencord-companion"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
18
package.json
18
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.7.9",
|
"version": "1.8.6",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
@ -19,16 +19,17 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
|
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
|
||||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||||
|
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch",
|
||||||
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
||||||
|
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
|
||||||
"inject": "node scripts/runInstaller.mjs",
|
"inject": "node scripts/runInstaller.mjs",
|
||||||
|
"uninject": "node scripts/runInstaller.mjs",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
||||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
||||||
"lint:fix": "pnpm lint --fix",
|
"lint:fix": "pnpm lint --fix",
|
||||||
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
|
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
|
||||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||||
"testTsc": "tsc --noEmit",
|
"testTsc": "tsc --noEmit"
|
||||||
"uninject": "node scripts/runInstaller.mjs",
|
|
||||||
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
||||||
|
@ -65,13 +66,14 @@
|
||||||
"standalone-electron-types": "^1.0.0",
|
"standalone-electron-types": "^1.0.0",
|
||||||
"stylelint": "^15.6.0",
|
"stylelint": "^15.6.0",
|
||||||
"stylelint-config-standard": "^33.0.0",
|
"stylelint-config-standard": "^33.0.0",
|
||||||
|
"ts-patch": "^3.1.2",
|
||||||
"tsx": "^3.12.7",
|
"tsx": "^3.12.7",
|
||||||
"type-fest": "^3.9.0",
|
"type-fest": "^3.9.0",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.4.5",
|
||||||
"zip-local": "^0.3.5",
|
"typescript-transform-paths": "^3.4.7",
|
||||||
"zustand": "^3.7.2"
|
"zip-local": "^0.3.5"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.10.2",
|
"packageManager": "pnpm@9.1.0",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
||||||
|
|
7
packages/vencord-types/.gitignore
vendored
Normal file
7
packages/vencord-types/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
*
|
||||||
|
!.*ignore
|
||||||
|
!package.json
|
||||||
|
!*.md
|
||||||
|
!prepare.ts
|
||||||
|
!index.d.ts
|
||||||
|
!globals.d.ts
|
4
packages/vencord-types/.npmignore
Normal file
4
packages/vencord-types/.npmignore
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
prepare.ts
|
||||||
|
.gitignore
|
||||||
|
HOW2PUB.md
|
5
packages/vencord-types/HOW2PUB.md
Normal file
5
packages/vencord-types/HOW2PUB.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# How to publish
|
||||||
|
|
||||||
|
1. run `pnpm generateTypes` in the project root
|
||||||
|
2. bump package.json version
|
||||||
|
3. npm publish
|
11
packages/vencord-types/README.md
Normal file
11
packages/vencord-types/README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Vencord Types
|
||||||
|
|
||||||
|
Typings for Vencord's api, published to npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i @vencord/types
|
||||||
|
|
||||||
|
yarn add @vencord/types
|
||||||
|
|
||||||
|
pnpm add @vencord/types
|
||||||
|
```
|
24
packages/vencord-types/globals.d.ts
vendored
Normal file
24
packages/vencord-types/globals.d.ts
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
export var VencordNative: typeof import("./VencordNative").default;
|
||||||
|
export var Vencord: typeof import("./Vencord");
|
||||||
|
}
|
||||||
|
|
||||||
|
export { };
|
5
packages/vencord-types/index.d.ts
vendored
Normal file
5
packages/vencord-types/index.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
/// <reference path="Vencord.d.ts" />
|
||||||
|
/// <reference path="globals.d.ts" />
|
||||||
|
/// <reference path="modules.d.ts" />
|
28
packages/vencord-types/package.json
Normal file
28
packages/vencord-types/package.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "@vencord/types",
|
||||||
|
"private": false,
|
||||||
|
"version": "0.1.3",
|
||||||
|
"description": "",
|
||||||
|
"types": "index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "tsx ./prepare.ts",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Vencord",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
|
"tsx": "^3.12.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash": "^4.14.191",
|
||||||
|
"@types/node": "^18.11.18",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.0.10",
|
||||||
|
"discord-types": "^1.3.26",
|
||||||
|
"standalone-electron-types": "^1.0.0",
|
||||||
|
"type-fest": "^3.5.3"
|
||||||
|
}
|
||||||
|
}
|
47
packages/vencord-types/prepare.ts
Normal file
47
packages/vencord-types/prepare.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { cpSync, moveSync, readdirSync, rmSync } from "fs-extra";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
readdirSync(join(__dirname, "src"))
|
||||||
|
.forEach(child => moveSync(join(__dirname, "src", child), join(__dirname, child), { overwrite: true }));
|
||||||
|
|
||||||
|
const VencordSrc = join(__dirname, "..", "..", "src");
|
||||||
|
|
||||||
|
for (const file of ["preload.d.ts", "userplugins", "main", "debug", "src", "browser", "scripts"]) {
|
||||||
|
rmSync(join(__dirname, file), { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyDtsFiles(from: string, to: string) {
|
||||||
|
for (const file of readdirSync(from, { withFileTypes: true })) {
|
||||||
|
// bad
|
||||||
|
if (from === VencordSrc && file.name === "globals.d.ts") continue;
|
||||||
|
|
||||||
|
const fullFrom = join(from, file.name);
|
||||||
|
const fullTo = join(to, file.name);
|
||||||
|
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
copyDtsFiles(fullFrom, fullTo);
|
||||||
|
} else if (file.name.endsWith(".d.ts")) {
|
||||||
|
cpSync(fullFrom, fullTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyDtsFiles(VencordSrc, __dirname);
|
5434
pnpm-lock.yaml
generated
5434
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
packages:
|
||||||
|
- packages/*
|
|
@ -25,10 +25,11 @@ import { access, readdir, readFile } from "fs/promises";
|
||||||
import { join, relative } from "path";
|
import { join, relative } from "path";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
// wtf is this assert syntax
|
|
||||||
import PackageJSON from "../../package.json" assert { type: "json" };
|
|
||||||
import { getPluginTarget } from "../utils.mjs";
|
import { getPluginTarget } from "../utils.mjs";
|
||||||
|
|
||||||
|
/** @type {import("../../package.json")} */
|
||||||
|
const PackageJSON = JSON.parse(readFileSync("package.json"));
|
||||||
|
|
||||||
export const VERSION = PackageJSON.version;
|
export const VERSION = PackageJSON.version;
|
||||||
// https://reproducible-builds.org/docs/source-date-epoch/
|
// https://reproducible-builds.org/docs/source-date-epoch/
|
||||||
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
|
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
|
||||||
|
|
|
@ -243,19 +243,27 @@ page.on("console", async e => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDebug) {
|
async function getText() {
|
||||||
console.error(e.text());
|
try {
|
||||||
} else if (level === "error") {
|
return await Promise.all(
|
||||||
const text = await Promise.all(
|
e.args().map(async a => {
|
||||||
e.args().map(async a => {
|
|
||||||
try {
|
|
||||||
return await maybeGetError(a) || await a.jsonValue();
|
return await maybeGetError(a) || await a.jsonValue();
|
||||||
} catch (e) {
|
})
|
||||||
return a.toString();
|
).then(a => a.join(" ").trim());
|
||||||
}
|
} catch {
|
||||||
})
|
return e.text();
|
||||||
).then(a => a.join(" ").trim());
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
const text = await getText();
|
||||||
|
|
||||||
|
console.error(text);
|
||||||
|
if (text.includes("A fatal error occurred:")) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else if (level === "error") {
|
||||||
|
const text = await getText();
|
||||||
|
|
||||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
||||||
console.error("[Unexpected Error]", text);
|
console.error("[Unexpected Error]", text);
|
||||||
|
@ -269,7 +277,7 @@ page.on("pageerror", e => console.error("[Page Error]", e));
|
||||||
|
|
||||||
await page.setBypassCSP(true);
|
await page.setBypassCSP(true);
|
||||||
|
|
||||||
function runTime(token: string) {
|
async function runtime(token: string) {
|
||||||
console.log("[PUP_DEBUG]", "Starting test...");
|
console.log("[PUP_DEBUG]", "Starting test...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -282,9 +290,13 @@ function runTime(token: string) {
|
||||||
|
|
||||||
// Monkey patch Logger to not log with custom css
|
// Monkey patch Logger to not log with custom css
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
const originalLog = Vencord.Util.Logger.prototype._log;
|
||||||
|
// @ts-ignore
|
||||||
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
|
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
|
||||||
if (level === "warn" || level === "error")
|
if (level === "warn" || level === "error")
|
||||||
console[level]("[Vencord]", this.name + ":", ...args);
|
return console[level]("[Vencord]", this.name + ":", ...args);
|
||||||
|
|
||||||
|
return originalLog.call(this, level, levelColor, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Force enable all plugins and patches
|
// Force enable all plugins and patches
|
||||||
|
@ -299,8 +311,10 @@ function runTime(token: string) {
|
||||||
delete patch.predicate;
|
delete patch.predicate;
|
||||||
delete patch.group;
|
delete patch.group;
|
||||||
|
|
||||||
if (!Array.isArray(patch.replacement))
|
Vencord.Util.canonicalizeFind(patch);
|
||||||
|
if (!Array.isArray(patch.replacement)) {
|
||||||
patch.replacement = [patch.replacement];
|
patch.replacement = [patch.replacement];
|
||||||
|
}
|
||||||
|
|
||||||
patch.replacement.forEach(r => {
|
patch.replacement.forEach(r => {
|
||||||
delete r.predicate;
|
delete r.predicate;
|
||||||
|
@ -310,45 +324,39 @@ function runTime(token: string) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Vencord.Webpack.waitFor(
|
let wreq: typeof Vencord.Webpack.wreq;
|
||||||
"loginToken",
|
|
||||||
m => {
|
|
||||||
console.log("[PUP_DEBUG]", "Logging in with token...");
|
|
||||||
m.loginToken(token);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Force load all chunks
|
const { canonicalizeMatch, Logger } = Vencord.Util;
|
||||||
Vencord.Webpack.onceReady.then(() => setTimeout(async () => {
|
|
||||||
console.log("[PUP_DEBUG]", "Webpack is ready!");
|
|
||||||
|
|
||||||
const { wreq } = Vencord.Webpack;
|
const validChunks = new Set<string>();
|
||||||
|
const invalidChunks = new Set<string>();
|
||||||
|
const deferredRequires = new Set<string>();
|
||||||
|
|
||||||
console.log("[PUP_DEBUG]", "Loading all chunks...");
|
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
||||||
|
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
||||||
|
|
||||||
let chunks = null as Record<number, string[]> | null;
|
// True if resolved, false otherwise
|
||||||
const sym = Symbol("Vencord.chunksExtract");
|
const chunksSearchPromises = [] as Array<() => boolean>;
|
||||||
|
|
||||||
Object.defineProperty(Object.prototype, sym, {
|
const LazyChunkRegex = canonicalizeMatch(/(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\)))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
|
||||||
get() {
|
|
||||||
chunks = this;
|
|
||||||
},
|
|
||||||
set() { },
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await (wreq as any).el(sym);
|
async function searchAndLoadLazyChunks(factoryCode: string) {
|
||||||
delete Object.prototype[sym];
|
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
||||||
|
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
|
||||||
|
|
||||||
const validChunksEntryPoints = new Set<string>();
|
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
|
||||||
const validChunks = new Set<string>();
|
// the chunk containing the component
|
||||||
const invalidChunks = new Set<string>();
|
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
|
||||||
|
|
||||||
if (!chunks) throw new Error("Failed to get chunks");
|
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIdsArray, rawChunkIdsSingle, entryPoint]) => {
|
||||||
|
const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
|
||||||
|
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Vencord.Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
|
||||||
|
|
||||||
for (const entryPoint in chunks) {
|
if (chunkIds.length === 0) {
|
||||||
const chunkIds = chunks[entryPoint];
|
return;
|
||||||
let invalidEntryPoint = false;
|
}
|
||||||
|
|
||||||
|
let invalidChunkGroup = false;
|
||||||
|
|
||||||
for (const id of chunkIds) {
|
for (const id of chunkIds) {
|
||||||
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
||||||
|
@ -359,121 +367,187 @@ function runTime(token: string) {
|
||||||
|
|
||||||
if (isWasm) {
|
if (isWasm) {
|
||||||
invalidChunks.add(id);
|
invalidChunks.add(id);
|
||||||
invalidEntryPoint = true;
|
invalidChunkGroup = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
validChunks.add(id);
|
validChunks.add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!invalidEntryPoint)
|
if (!invalidChunkGroup) {
|
||||||
validChunksEntryPoints.add(entryPoint);
|
validChunkGroups.add([chunkIds, entryPoint]);
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
for (const entryPoint of validChunksEntryPoints) {
|
// Loads all found valid chunk groups
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(validChunkGroups)
|
||||||
|
.map(([chunkIds]) =>
|
||||||
|
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Requires the entry points for all valid chunk groups
|
||||||
|
for (const [, entryPoint] of validChunkGroups) {
|
||||||
try {
|
try {
|
||||||
// Loads all chunks required for an entry point
|
if (shouldForceDefer) {
|
||||||
await (wreq as any).el(entryPoint);
|
deferredRequires.add(entryPoint);
|
||||||
} catch (err) { }
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matches "id" or id:
|
|
||||||
const chunkIdRegex = /(?:"(\d+?)")|(?:(\d+?):)/g;
|
|
||||||
const wreqU = wreq.u.toString();
|
|
||||||
|
|
||||||
const allChunks = [] as string[];
|
|
||||||
let currentMatch: RegExpExecArray | null;
|
|
||||||
|
|
||||||
while ((currentMatch = chunkIdRegex.exec(wreqU)) != null) {
|
|
||||||
const id = currentMatch[1] ?? currentMatch[2];
|
|
||||||
if (id == null) continue;
|
|
||||||
|
|
||||||
allChunks.push(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
|
||||||
const chunksLeft = allChunks.filter(id => {
|
|
||||||
return !(validChunks.has(id) || invalidChunks.has(id));
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const id of chunksLeft) {
|
|
||||||
const isWasm = await fetch(wreq.p + wreq.u(id))
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
|
||||||
|
|
||||||
// Loads a chunk
|
|
||||||
if (!isWasm) await wreq.e(id as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure every chunk has finished loading
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
|
||||||
|
|
||||||
for (const entryPoint of validChunksEntryPoints) {
|
|
||||||
try {
|
|
||||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[PUP_DEBUG]", "Finished loading all chunks!");
|
// setImmediate to only check if all chunks were loaded after this function resolves
|
||||||
|
// We check if all chunks were loaded every time a factory is loaded
|
||||||
|
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
|
||||||
|
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
|
||||||
|
setTimeout(() => {
|
||||||
|
let allResolved = true;
|
||||||
|
|
||||||
for (const patch of Vencord.Plugins.patches) {
|
for (let i = 0; i < chunksSearchPromises.length; i++) {
|
||||||
if (!patch.all) {
|
const isResolved = chunksSearchPromises[i]();
|
||||||
new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) {
|
if (isResolved) {
|
||||||
let method = searchType;
|
// Remove finished promises to avoid having to iterate through a huge array everytime
|
||||||
|
chunksSearchPromises.splice(i--, 1);
|
||||||
if (searchType === "findComponent") method = "find";
|
|
||||||
if (searchType === "findExportedComponent") method = "findByProps";
|
|
||||||
if (searchType === "waitFor" || searchType === "waitForComponent") {
|
|
||||||
if (typeof args[0] === "string") method = "findByProps";
|
|
||||||
else method = "find";
|
|
||||||
}
|
|
||||||
if (searchType === "waitForStore") method = "findStore";
|
|
||||||
|
|
||||||
try {
|
|
||||||
let result: any;
|
|
||||||
|
|
||||||
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
|
||||||
const [factory] = args;
|
|
||||||
result = factory();
|
|
||||||
} else if (method === "extractAndLoadChunks") {
|
|
||||||
const [code, matcher] = args;
|
|
||||||
|
|
||||||
const module = Vencord.Webpack.findModuleFactory(...code);
|
|
||||||
if (module) result = module.toString().match(Vencord.Util.canonicalizeMatch(matcher));
|
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
allResolved = false;
|
||||||
result = Vencord.Webpack[method](...args);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
|
|
||||||
} catch (e) {
|
|
||||||
let logMessage = searchType;
|
|
||||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
|
||||||
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
|
||||||
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
|
||||||
|
|
||||||
console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
|
if (allResolved) chunksSearchingResolve();
|
||||||
}, 1000));
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vencord.Webpack.waitFor(
|
||||||
|
"loginToken",
|
||||||
|
m => {
|
||||||
|
console.log("[PUP_DEBUG]", "Logging in with token...");
|
||||||
|
m.loginToken(token);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Vencord.Webpack.beforeInitListeners.add(async webpackRequire => {
|
||||||
|
console.log("[PUP_DEBUG]", "Loading all chunks...");
|
||||||
|
|
||||||
|
wreq = webpackRequire;
|
||||||
|
|
||||||
|
Vencord.Webpack.factoryListeners.add(factory => {
|
||||||
|
let isResolved = false;
|
||||||
|
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
||||||
|
|
||||||
|
chunksSearchPromises.push(() => isResolved);
|
||||||
|
});
|
||||||
|
|
||||||
|
// setImmediate to only search the initial factories after Discord initialized the app
|
||||||
|
// our beforeInitListeners are called before Discord initializes the app
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const factoryId in wreq.m) {
|
||||||
|
let isResolved = false;
|
||||||
|
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
||||||
|
|
||||||
|
chunksSearchPromises.push(() => isResolved);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
await chunksSearchingDone;
|
||||||
|
|
||||||
|
// Require deferred entry points
|
||||||
|
for (const deferredRequire of deferredRequires) {
|
||||||
|
wreq!(deferredRequire as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
||||||
|
const allChunks = [] as string[];
|
||||||
|
|
||||||
|
// Matches "id" or id:
|
||||||
|
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
||||||
|
const id = currentMatch[1] ?? currentMatch[2];
|
||||||
|
if (id == null) continue;
|
||||||
|
|
||||||
|
allChunks.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
||||||
|
|
||||||
|
// Chunks that are not loaded (not used) by Discord code anymore
|
||||||
|
const chunksLeft = allChunks.filter(id => {
|
||||||
|
return !(validChunks.has(id) || invalidChunks.has(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(chunksLeft.map(async id => {
|
||||||
|
const isWasm = await fetch(wreq.p + wreq.u(id))
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
||||||
|
|
||||||
|
// Loads and requires a chunk
|
||||||
|
if (!isWasm) {
|
||||||
|
await wreq.e(id as any);
|
||||||
|
if (wreq.m[id]) wreq(id as any);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log("[PUP_DEBUG]", "Finished loading all chunks!");
|
||||||
|
|
||||||
|
for (const patch of Vencord.Plugins.patches) {
|
||||||
|
if (!patch.all) {
|
||||||
|
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) {
|
||||||
|
let method = searchType;
|
||||||
|
|
||||||
|
if (searchType === "findComponent") method = "find";
|
||||||
|
if (searchType === "findExportedComponent") method = "findByProps";
|
||||||
|
if (searchType === "waitFor" || searchType === "waitForComponent") {
|
||||||
|
if (typeof args[0] === "string") method = "findByProps";
|
||||||
|
else method = "find";
|
||||||
|
}
|
||||||
|
if (searchType === "waitForStore") method = "findStore";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result: any;
|
||||||
|
|
||||||
|
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
||||||
|
const [factory] = args;
|
||||||
|
result = factory();
|
||||||
|
} else if (method === "extractAndLoadChunks") {
|
||||||
|
const [code, matcher] = args;
|
||||||
|
|
||||||
|
const module = Vencord.Webpack.findModuleFactory(...code);
|
||||||
|
if (module) result = module.toString().match(canonicalizeMatch(matcher));
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
result = Vencord.Webpack[method](...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
|
||||||
|
} catch (e) {
|
||||||
|
let logMessage = searchType;
|
||||||
|
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
||||||
|
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
||||||
|
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
||||||
|
|
||||||
|
console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
|
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.evaluateOnNewDocument(`
|
await page.evaluateOnNewDocument(`
|
||||||
${readFileSync("./dist/browser.js", "utf-8")}
|
${readFileSync("./dist/browser.js", "utf-8")}
|
||||||
|
|
||||||
;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
;(${runtime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
|
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * as Api from "./api";
|
export * as Api from "./api";
|
||||||
|
export * as Components from "./components";
|
||||||
export * as Plugins from "./plugins";
|
export * as Plugins from "./plugins";
|
||||||
export * as Util from "./utils";
|
export * as Util from "./utils";
|
||||||
export * as QuickCss from "./utils/quickCss";
|
export * as QuickCss from "./utils/quickCss";
|
||||||
|
|
|
@ -36,7 +36,7 @@ export interface ProfileBadge {
|
||||||
image?: string;
|
image?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
/** Action to perform when you click the badge */
|
/** Action to perform when you click the badge */
|
||||||
onClick?(): void;
|
onClick?(event: React.MouseEvent<HTMLButtonElement, MouseEvent>, props: BadgeUserArgs): void;
|
||||||
/** Should the user display this badge? */
|
/** Should the user display this badge? */
|
||||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||||
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||||
|
@ -87,9 +87,7 @@ export function _getBadges(args: BadgeUserArgs) {
|
||||||
|
|
||||||
export interface BadgeUserArgs {
|
export interface BadgeUserArgs {
|
||||||
user: User;
|
user: User;
|
||||||
profile: Profile;
|
guildId: string;
|
||||||
premiumSince: Date;
|
|
||||||
premiumGuildSince?: Date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectedAccount {
|
interface ConnectedAccount {
|
||||||
|
|
|
@ -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 { mergeDefaults } from "@utils/misc";
|
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { MessageActions, SnowflakeUtils } from "@webpack/common";
|
import { MessageActions, SnowflakeUtils } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
|
|
@ -100,6 +100,7 @@ export async function showNotification(data: NotificationData) {
|
||||||
const n = new Notification(title, {
|
const n = new Notification(title, {
|
||||||
body,
|
body,
|
||||||
icon,
|
icon,
|
||||||
|
// @ts-expect-error ts is drunk
|
||||||
image
|
image
|
||||||
});
|
});
|
||||||
n.onclick = onClick;
|
n.onclick = onClick;
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { debounce } from "@shared/debounce";
|
||||||
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
|
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
|
||||||
import { localStorage } from "@utils/localStorage";
|
import { localStorage } from "@utils/localStorage";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { mergeDefaults } from "@utils/misc";
|
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||||
import { putCloudSettings } from "@utils/settingsSync";
|
import { putCloudSettings } from "@utils/settingsSync";
|
||||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||||
import { React } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
|
@ -16,10 +16,12 @@
|
||||||
* 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 "./ExpandableHeader.css";
|
||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { Text, Tooltip, useState } from "@webpack/common";
|
import { Text, Tooltip, useState } from "@webpack/common";
|
||||||
export const cl = classNameFactory("vc-expandableheader-");
|
|
||||||
import "./ExpandableHeader.css";
|
const cl = classNameFactory("vc-expandableheader-");
|
||||||
|
|
||||||
export interface ExpandableHeaderProps {
|
export interface ExpandableHeaderProps {
|
||||||
onMoreClick?: () => void;
|
onMoreClick?: () => void;
|
||||||
|
@ -31,7 +33,7 @@ export interface ExpandableHeaderProps {
|
||||||
buttons?: React.ReactNode[];
|
buttons?: React.ReactNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
|
export function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
|
||||||
const [showContent, setShowContent] = useState(defaultState);
|
const [showContent, setShowContent] = useState(defaultState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -9,10 +9,12 @@ import "./contributorModal.css";
|
||||||
import { useSettings } from "@api/Settings";
|
import { useSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Link } from "@components/Link";
|
||||||
import { DevsById } from "@utils/constants";
|
import { DevsById } from "@utils/constants";
|
||||||
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
||||||
|
import { pluralise } from "@utils/misc";
|
||||||
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
||||||
import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
import { Forms, MaskedLink, showToast, Tooltip, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
@ -72,6 +74,8 @@ function ContributorModal({ user }: { user: User; }) {
|
||||||
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
||||||
}, [user.id, user.username]);
|
}, [user.id, user.username]);
|
||||||
|
|
||||||
|
const ContributedHyperLink = <Link href="https://vencord.dev/source">contributed</Link>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={cl("header")}>
|
<div className={cl("header")}>
|
||||||
|
@ -84,30 +88,48 @@ function ContributorModal({ user }: { user: User; }) {
|
||||||
|
|
||||||
<div className={cl("links")}>
|
<div className={cl("links")}>
|
||||||
{website && (
|
{website && (
|
||||||
<MaskedLink
|
<Tooltip text={website}>
|
||||||
href={"https://" + website}
|
{props => (
|
||||||
>
|
<MaskedLink {...props} href={"https://" + website}>
|
||||||
<WebsiteIcon />
|
<WebsiteIcon />
|
||||||
</MaskedLink>
|
</MaskedLink>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{githubName && (
|
{githubName && (
|
||||||
<MaskedLink href={`https://github.com/${githubName}`}>
|
<Tooltip text={githubName}>
|
||||||
<GithubIcon />
|
{props => (
|
||||||
</MaskedLink>
|
<MaskedLink {...props} href={`https://github.com/${githubName}`}>
|
||||||
|
<GithubIcon />
|
||||||
|
</MaskedLink>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cl("plugins")}>
|
{plugins.length ? (
|
||||||
{plugins.map(p =>
|
<Forms.FormText>
|
||||||
<PluginCard
|
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
|
||||||
key={p.name}
|
</Forms.FormText>
|
||||||
plugin={p}
|
) : (
|
||||||
disabled={p.required ?? false}
|
<Forms.FormText>
|
||||||
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
|
||||||
/>
|
</Forms.FormText>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
{!!plugins.length && (
|
||||||
|
<div className={cl("plugins")}>
|
||||||
|
{plugins.map(p =>
|
||||||
|
<PluginCard
|
||||||
|
key={p.name}
|
||||||
|
plugin={p}
|
||||||
|
disabled={p.required ?? false}
|
||||||
|
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,11 +25,13 @@
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 16px;
|
width: 32px;
|
||||||
background: var(--background-tertiary);
|
background: var(--background-tertiary);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
left: -16px;
|
left: -32px;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
border-top-left-radius: 9999px;
|
||||||
|
border-bottom-left-radius: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-author-modal-avatar {
|
.vc-author-modal-avatar {
|
||||||
|
@ -55,4 +57,5 @@
|
||||||
.vc-author-modal-plugins {
|
.vc-author-modal-plugins {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
margin-top: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import PluginModal from "@components/PluginSettings/PluginModal";
|
||||||
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
||||||
import { SettingsTab } from "@components/VencordSettings/shared";
|
import { SettingsTab } from "@components/VencordSettings/shared";
|
||||||
import { ChangeList } from "@utils/ChangeList";
|
import { ChangeList } from "@utils/ChangeList";
|
||||||
|
import { proxyLazy } from "@utils/lazy";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes, isObjectEmpty } from "@utils/misc";
|
import { classes, isObjectEmpty } from "@utils/misc";
|
||||||
|
@ -38,8 +39,8 @@ import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextI
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
// Avoid circular dependency
|
||||||
|
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins"));
|
||||||
|
|
||||||
const cl = classNameFactory("vc-plugins-");
|
const cl = classNameFactory("vc-plugins-");
|
||||||
const logger = new Logger("PluginSettings", "#a6d189");
|
const logger = new Logger("PluginSettings", "#a6d189");
|
||||||
|
|
|
@ -21,7 +21,7 @@ import "./addonCard.css";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { Badge } from "@components/Badge";
|
import { Badge } from "@components/Badge";
|
||||||
import { Switch } from "@components/Switch";
|
import { Switch } from "@components/Switch";
|
||||||
import { Text } from "@webpack/common";
|
import { Text, useRef } from "@webpack/common";
|
||||||
import type { MouseEventHandler, ReactNode } from "react";
|
import type { MouseEventHandler, ReactNode } from "react";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-addon-");
|
const cl = classNameFactory("vc-addon-");
|
||||||
|
@ -42,6 +42,8 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
||||||
|
const titleRef = useRef<HTMLDivElement>(null);
|
||||||
|
const titleContainerRef = useRef<HTMLDivElement>(null);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cl("card", { "card-disabled": disabled })}
|
className={cl("card", { "card-disabled": disabled })}
|
||||||
|
@ -51,7 +53,21 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
|
||||||
<div className={cl("header")}>
|
<div className={cl("header")}>
|
||||||
<div className={cl("name-author")}>
|
<div className={cl("name-author")}>
|
||||||
<Text variant="text-md/bold" className={cl("name")}>
|
<Text variant="text-md/bold" className={cl("name")}>
|
||||||
{name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
<div ref={titleContainerRef} className={cl("title-container")}>
|
||||||
|
<div
|
||||||
|
ref={titleRef}
|
||||||
|
className={cl("title")}
|
||||||
|
onMouseOver={() => {
|
||||||
|
const title = titleRef.current!;
|
||||||
|
const titleContainer = titleContainerRef.current!;
|
||||||
|
|
||||||
|
title.style.setProperty("--offset", `${titleContainer.clientWidth - title.scrollWidth}px`);
|
||||||
|
title.style.setProperty("--duration", `${Math.max(0.5, (title.scrollWidth - titleContainer.clientWidth) / 7)}s`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||||
</Text>
|
</Text>
|
||||||
{!!author && (
|
{!!author && (
|
||||||
<Text variant="text-md/normal" className={cl("author")}>
|
<Text variant="text-md/normal" className={cl("author")}>
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
* 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 { CheckedTextInput } from "@components/CheckedTextInput";
|
|
||||||
import { CodeBlock } from "@components/CodeBlock";
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
import { debounce } from "@shared/debounce";
|
import { debounce } from "@shared/debounce";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
|
@ -47,7 +46,7 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
|
||||||
|
|
||||||
interface ReplacementComponentProps {
|
interface ReplacementComponentProps {
|
||||||
module: [id: number, factory: Function];
|
module: [id: number, factory: Function];
|
||||||
match: string | RegExp;
|
match: string;
|
||||||
replacement: string | ReplaceFn;
|
replacement: string | ReplaceFn;
|
||||||
setReplacementError(error: any): void;
|
setReplacementError(error: any): void;
|
||||||
}
|
}
|
||||||
|
@ -58,7 +57,13 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
||||||
|
|
||||||
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
||||||
const src: string = fact.toString().replaceAll("\n", "");
|
const src: string = fact.toString().replaceAll("\n", "");
|
||||||
const canonicalMatch = canonicalizeMatch(match);
|
|
||||||
|
try {
|
||||||
|
new RegExp(match);
|
||||||
|
} catch (e) {
|
||||||
|
return ["", [], []];
|
||||||
|
}
|
||||||
|
const canonicalMatch = canonicalizeMatch(new RegExp(match));
|
||||||
try {
|
try {
|
||||||
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
||||||
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
||||||
|
@ -180,7 +185,8 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle>replacement</Forms.FormTitle>
|
{/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
|
||||||
|
<Forms.FormTitle className="">replacement</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={replacement?.toString()}
|
value={replacement?.toString()}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -188,7 +194,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||||
/>
|
/>
|
||||||
{!isFunc && (
|
{!isFunc && (
|
||||||
<div className="vc-text-selectable">
|
<div className="vc-text-selectable">
|
||||||
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>
|
||||||
{Object.entries({
|
{Object.entries({
|
||||||
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
||||||
"$$": "Insert a $",
|
"$$": "Insert a $",
|
||||||
|
@ -220,11 +226,12 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||||
|
|
||||||
interface FullPatchInputProps {
|
interface FullPatchInputProps {
|
||||||
setFind(v: string): void;
|
setFind(v: string): void;
|
||||||
|
setParsedFind(v: string | RegExp): void;
|
||||||
setMatch(v: string): void;
|
setMatch(v: string): void;
|
||||||
setReplacement(v: string | ReplaceFn): void;
|
setReplacement(v: string | ReplaceFn): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputProps) {
|
function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
|
||||||
const [fullPatch, setFullPatch] = React.useState<string>("");
|
const [fullPatch, setFullPatch] = React.useState<string>("");
|
||||||
const [fullPatchError, setFullPatchError] = React.useState<string>("");
|
const [fullPatchError, setFullPatchError] = React.useState<string>("");
|
||||||
|
|
||||||
|
@ -233,6 +240,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
||||||
setFullPatchError("");
|
setFullPatchError("");
|
||||||
|
|
||||||
setFind("");
|
setFind("");
|
||||||
|
setParsedFind("");
|
||||||
setMatch("");
|
setMatch("");
|
||||||
setReplacement("");
|
setReplacement("");
|
||||||
return;
|
return;
|
||||||
|
@ -256,7 +264,8 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
||||||
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
|
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
|
||||||
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
|
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
|
||||||
|
|
||||||
setFind(parsed.find);
|
setFind(parsed.find instanceof RegExp ? parsed.find.toString() : parsed.find);
|
||||||
|
setParsedFind(parsed.find);
|
||||||
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
|
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
|
||||||
setReplacement(parsed.replacement.replace);
|
setReplacement(parsed.replacement.replace);
|
||||||
setFullPatchError("");
|
setFullPatchError("");
|
||||||
|
@ -266,7 +275,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Forms.FormText>Paste your full JSON patch here to fill out the fields</Forms.FormText>
|
<Forms.FormText className={Margins.bottom8}>Paste your full JSON patch here to fill out the fields</Forms.FormText>
|
||||||
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
|
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
|
||||||
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
|
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
|
||||||
</>;
|
</>;
|
||||||
|
@ -274,6 +283,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
||||||
|
|
||||||
function PatchHelper() {
|
function PatchHelper() {
|
||||||
const [find, setFind] = React.useState<string>("");
|
const [find, setFind] = React.useState<string>("");
|
||||||
|
const [parsedFind, setParsedFind] = React.useState<string | RegExp>("");
|
||||||
const [match, setMatch] = React.useState<string>("");
|
const [match, setMatch] = React.useState<string>("");
|
||||||
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
||||||
|
|
||||||
|
@ -281,34 +291,46 @@ function PatchHelper() {
|
||||||
|
|
||||||
const [module, setModule] = React.useState<[number, Function]>();
|
const [module, setModule] = React.useState<[number, Function]>();
|
||||||
const [findError, setFindError] = React.useState<string>();
|
const [findError, setFindError] = React.useState<string>();
|
||||||
|
const [matchError, setMatchError] = React.useState<string>();
|
||||||
|
|
||||||
const code = React.useMemo(() => {
|
const code = React.useMemo(() => {
|
||||||
return `
|
return `
|
||||||
{
|
{
|
||||||
find: ${JSON.stringify(find)},
|
find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
||||||
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`.trim();
|
`.trim();
|
||||||
}, [find, match, replacement]);
|
}, [parsedFind, match, replacement]);
|
||||||
|
|
||||||
function onFindChange(v: string) {
|
function onFindChange(v: string) {
|
||||||
setFindError(void 0);
|
|
||||||
setFind(v);
|
setFind(v);
|
||||||
if (v.length) {
|
|
||||||
findCandidates({ find: v, setModule, setError: setFindError });
|
try {
|
||||||
|
let parsedFind = v as string | RegExp;
|
||||||
|
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
|
||||||
|
|
||||||
|
setFindError(void 0);
|
||||||
|
setParsedFind(parsedFind);
|
||||||
|
|
||||||
|
if (v.length) {
|
||||||
|
findCandidates({ find: parsedFind, setModule, setError: setFindError });
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setFindError((e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMatchChange(v: string) {
|
function onMatchChange(v: string) {
|
||||||
|
setMatch(v);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
new RegExp(v);
|
new RegExp(v);
|
||||||
setFindError(void 0);
|
setMatchError(void 0);
|
||||||
setMatch(v);
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setFindError((e as Error).message);
|
setMatchError((e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,11 +339,12 @@ function PatchHelper() {
|
||||||
<Forms.FormTitle>full patch</Forms.FormTitle>
|
<Forms.FormTitle>full patch</Forms.FormTitle>
|
||||||
<FullPatchInput
|
<FullPatchInput
|
||||||
setFind={onFindChange}
|
setFind={onFindChange}
|
||||||
|
setParsedFind={setParsedFind}
|
||||||
setMatch={onMatchChange}
|
setMatch={onMatchChange}
|
||||||
setReplacement={setReplacement}
|
setReplacement={setReplacement}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Forms.FormTitle>find</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top8}>find</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
value={find}
|
value={find}
|
||||||
|
@ -329,19 +352,15 @@ function PatchHelper() {
|
||||||
error={findError}
|
error={findError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Forms.FormTitle>match</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
||||||
<CheckedTextInput
|
<TextInput
|
||||||
|
type="text"
|
||||||
value={match}
|
value={match}
|
||||||
onChange={onMatchChange}
|
onChange={onMatchChange}
|
||||||
validate={v => {
|
error={matchError}
|
||||||
try {
|
|
||||||
return (new RegExp(v), true);
|
|
||||||
} catch (e) {
|
|
||||||
return (e as Error).message;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className={Margins.top8} />
|
||||||
<ReplacementInput
|
<ReplacementInput
|
||||||
replacement={replacement}
|
replacement={replacement}
|
||||||
setReplacement={setReplacement}
|
setReplacement={setReplacement}
|
||||||
|
@ -352,7 +371,7 @@ function PatchHelper() {
|
||||||
{module && (
|
{module && (
|
||||||
<ReplacementComponent
|
<ReplacementComponent
|
||||||
module={module}
|
module={module}
|
||||||
match={new RegExp(match)}
|
match={match}
|
||||||
replacement={replacement}
|
replacement={replacement}
|
||||||
setReplacementError={setReplacementError}
|
setReplacementError={setReplacementError}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -62,3 +62,36 @@
|
||||||
.vc-addon-author::before {
|
.vc-addon-author::before {
|
||||||
content: "by ";
|
content: "by ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vc-addon-title-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 1.25em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-addon-title {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes vc-addon-title {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateX(var(--offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-addon-title:hover {
|
||||||
|
overflow: visible;
|
||||||
|
animation: vc-addon-title var(--duration) linear infinite;
|
||||||
|
}
|
||||||
|
|
18
src/components/index.ts
Normal file
18
src/components/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./Badge";
|
||||||
|
export * from "./CheckedTextInput";
|
||||||
|
export * from "./CodeBlock";
|
||||||
|
export * from "./DonateButton";
|
||||||
|
export { default as ErrorBoundary } from "./ErrorBoundary";
|
||||||
|
export * from "./ErrorCard";
|
||||||
|
export * from "./ExpandableHeader";
|
||||||
|
export * from "./Flex";
|
||||||
|
export * from "./Heart";
|
||||||
|
export * from "./Icons";
|
||||||
|
export * from "./Link";
|
||||||
|
export * from "./Switch";
|
|
@ -73,6 +73,9 @@ if (!IS_VANILLA) {
|
||||||
const original = options.webPreferences.preload;
|
const original = options.webPreferences.preload;
|
||||||
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
|
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
|
||||||
options.webPreferences.sandbox = false;
|
options.webPreferences.sandbox = false;
|
||||||
|
// work around discord unloading when in background
|
||||||
|
options.webPreferences.backgroundThrottling = false;
|
||||||
|
|
||||||
if (settings.frameless) {
|
if (settings.frameless) {
|
||||||
options.frame = false;
|
options.frame = false;
|
||||||
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
||||||
|
@ -136,6 +139,9 @@ if (!IS_VANILLA) {
|
||||||
}
|
}
|
||||||
return originalAppend.apply(this, args);
|
return originalAppend.apply(this, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Work around discord unloading when in background
|
||||||
|
app.commandLine.appendSwitch("disable-renderer-backgrounding");
|
||||||
} else {
|
} else {
|
||||||
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import type { Settings } from "@api/Settings";
|
import type { Settings } from "@api/Settings";
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
import { SettingsStore } from "@shared/SettingsStore";
|
import { SettingsStore } from "@shared/SettingsStore";
|
||||||
|
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
|
||||||
|
@ -42,7 +43,22 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string
|
||||||
RendererSettings.setData(data, pathToNotify);
|
RendererSettings.setData(data, pathToNotify);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NativeSettings = new SettingsStore(readSettings("native", NATIVE_SETTINGS_FILE));
|
export interface NativeSettings {
|
||||||
|
plugins: {
|
||||||
|
[plugin: string]: {
|
||||||
|
[setting: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultNativeSettings: NativeSettings = {
|
||||||
|
plugins: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
|
||||||
|
mergeDefaults(nativeSettings, DefaultNativeSettings);
|
||||||
|
|
||||||
|
export const NativeSettings = new SettingsStore(nativeSettings);
|
||||||
|
|
||||||
NativeSettings.addGlobalChangeListener(() => {
|
NativeSettings.addGlobalChangeListener(() => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -35,6 +35,7 @@ export const ALLOWED_PROTOCOLS = [
|
||||||
"steam:",
|
"steam:",
|
||||||
"spotify:",
|
"spotify:",
|
||||||
"com.epicgames.launcher:",
|
"com.epicgames.launcher:",
|
||||||
|
"tidal:"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
|
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
|
||||||
|
|
2
src/modules.d.ts
vendored
2
src/modules.d.ts
vendored
|
@ -20,7 +20,7 @@
|
||||||
/// <reference types="standalone-electron-types"/>
|
/// <reference types="standalone-electron-types"/>
|
||||||
|
|
||||||
declare module "~plugins" {
|
declare module "~plugins" {
|
||||||
const plugins: Record<string, import("@utils/types").Plugin>;
|
const plugins: Record<string, import("./utils/types").Plugin>;
|
||||||
export default plugins;
|
export default plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
src/plugins/_api/badges/fixBadgeOverflow.css
Normal file
3
src/plugins/_api/badges/fixBadgeOverflow.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[class*="profileBadges"] {
|
||||||
|
flex: none;
|
||||||
|
}
|
|
@ -16,11 +16,14 @@
|
||||||
* 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 "./fixBadgeOverflow.css";
|
||||||
|
|
||||||
import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
||||||
import DonateButton from "@components/DonateButton";
|
import DonateButton from "@components/DonateButton";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { Heart } from "@components/Heart";
|
import { Heart } from "@components/Heart";
|
||||||
|
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { isPluginDev } from "@utils/misc";
|
import { isPluginDev } from "@utils/misc";
|
||||||
|
@ -34,14 +37,8 @@ const ContributorBadge: ProfileBadge = {
|
||||||
description: "Vencord Contributor",
|
description: "Vencord Contributor",
|
||||||
image: CONTRIBUTOR_BADGE,
|
image: CONTRIBUTOR_BADGE,
|
||||||
position: BadgePosition.START,
|
position: BadgePosition.START,
|
||||||
props: {
|
|
||||||
style: {
|
|
||||||
borderRadius: "50%",
|
|
||||||
transform: "scale(0.9)" // The image is a bit too big compared to default badges
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shouldShow: ({ user }) => isPluginDev(user.id),
|
shouldShow: ({ user }) => isPluginDev(user.id),
|
||||||
link: "https://github.com/Vendicated/Vencord"
|
onClick: (_, { user }) => openContributorModal(user)
|
||||||
};
|
};
|
||||||
|
|
||||||
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
|
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
|
||||||
|
@ -79,13 +76,13 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// replace their component with ours if applicable
|
// replace their component with ours if applicable
|
||||||
{
|
{
|
||||||
match: /(?<=text:(\i)\.description,spacing:12,)children:/,
|
match: /(?<=text:(\i)\.description,spacing:12,.{0,50})children:/,
|
||||||
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :"
|
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :"
|
||||||
},
|
},
|
||||||
// conditionally override their onClick with badge.onClick if it exists
|
// conditionally override their onClick with badge.onClick if it exists
|
||||||
{
|
{
|
||||||
match: /href:(\i)\.link/,
|
match: /href:(\i)\.link/,
|
||||||
replace: "...($1.onClick && { onClick: $1.onClick }),$&"
|
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, arguments[0]) }),$&"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -16,17 +16,31 @@
|
||||||
* 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 { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
disableAnalytics: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Disable Discord's tracking (analytics/'science')",
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoTrack",
|
name: "NoTrack",
|
||||||
description: "Disable Discord's tracking ('science'), metrics and Sentry crash reporting",
|
description: "Disable Discord's tracking (analytics/'science'), metrics and Sentry crash reporting",
|
||||||
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz, Devs.Arrow],
|
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz, Devs.Arrow],
|
||||||
required: true,
|
required: true,
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "AnalyticsActionHandlers.handle",
|
find: "AnalyticsActionHandlers.handle",
|
||||||
|
predicate: () => settings.store.disableAnalytics,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /^.+$/,
|
match: /^.+$/,
|
||||||
replace: "()=>{}",
|
replace: "()=>{}",
|
||||||
|
@ -44,11 +58,11 @@ export default definePlugin({
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /this\._intervalId=/,
|
match: /this\._intervalId=/,
|
||||||
replace: "this._intervalId=undefined&&"
|
replace: "this._intervalId=void 0&&"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(increment\(\i\){)/,
|
match: /(?:increment|distribution)\(\i(?:,\i)?\){/g,
|
||||||
replace: "$1return;"
|
replace: "$&return;"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,57 +17,91 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
|
import BackupAndRestoreTab from "@components/VencordSettings/BackupAndRestoreTab";
|
||||||
|
import CloudTab from "@components/VencordSettings/CloudTab";
|
||||||
|
import PatchHelperTab from "@components/VencordSettings/PatchHelperTab";
|
||||||
|
import PluginsTab from "@components/VencordSettings/PluginsTab";
|
||||||
|
import ThemesTab from "@components/VencordSettings/ThemesTab";
|
||||||
|
import UpdaterTab from "@components/VencordSettings/UpdaterTab";
|
||||||
|
import VencordTab from "@components/VencordSettings/VencordTab";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { React } from "@webpack/common";
|
import { i18n, React } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
|
type SectionType = "HEADER" | "DIVIDER" | "CUSTOM";
|
||||||
|
type SectionTypes = Record<SectionType, SectionType>;
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
description: "Adds Settings UI and debug info",
|
description: "Adds Settings UI and debug info",
|
||||||
authors: [Devs.Ven, Devs.Megu],
|
authors: [Devs.Ven, Devs.Megu],
|
||||||
required: true,
|
required: true,
|
||||||
|
|
||||||
patches: [{
|
patches: [
|
||||||
find: ".versionHash",
|
{
|
||||||
replacement: [
|
find: ".versionHash",
|
||||||
{
|
replacement: [
|
||||||
match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
|
{
|
||||||
replace: (m, component, props) => {
|
match: /\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
|
||||||
props = props.replace(/children:\[.+\]/, "");
|
replace: (m, component, props) => {
|
||||||
return `${m},Vencord.Plugins.plugins.Settings.makeInfoElements(${component}, ${props})`;
|
props = props.replace(/children:\[.+\]/, "");
|
||||||
|
return `${m},$self.makeInfoElements(${component}, ${props})`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /copyValue:\i\.join\(" "\)/,
|
||||||
|
replace: "$& + $self.getInfoString()"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Discord Stable
|
||||||
|
// FIXME: remove once change merged to stable
|
||||||
|
{
|
||||||
|
find: "Messages.ACTIVITY_SETTINGS",
|
||||||
|
replacement: {
|
||||||
|
get match() {
|
||||||
|
switch (Settings.plugins.Settings.settingsLocation) {
|
||||||
|
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/;
|
||||||
|
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/;
|
||||||
|
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/;
|
||||||
|
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
|
||||||
|
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
|
||||||
|
case "aboveActivity":
|
||||||
|
default:
|
||||||
|
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
replace: "...$self.makeSettingsCategories($1),$&"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "Messages.ACTIVITY_SETTINGS",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
|
||||||
|
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "useDefaultUserSettingsSections:function",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=useDefaultUserSettingsSections:function\(\){return )(\i)\}/,
|
||||||
|
replace: "$self.wrapSettingsHook($1)}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
|
||||||
|
replace: "$2.default.open($1);return;"
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}, {
|
|
||||||
find: "Messages.ACTIVITY_SETTINGS",
|
|
||||||
replacement: {
|
|
||||||
get match() {
|
|
||||||
switch (Settings.plugins.Settings.settingsLocation) {
|
|
||||||
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/;
|
|
||||||
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/;
|
|
||||||
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/;
|
|
||||||
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
|
|
||||||
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
|
|
||||||
case "aboveActivity":
|
|
||||||
default:
|
|
||||||
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
replace: "...$self.makeSettingsCategories($1),$&"
|
|
||||||
}
|
}
|
||||||
}, {
|
],
|
||||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
|
|
||||||
replace: "$2.default.open($1);return;"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[],
|
customSections: [] as ((SectionTypes: SectionTypes) => any)[],
|
||||||
|
|
||||||
makeSettingsCategories(SectionTypes: Record<string, unknown>) {
|
makeSettingsCategories(SectionTypes: SectionTypes) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
section: SectionTypes.HEADER,
|
section: SectionTypes.HEADER,
|
||||||
|
@ -77,43 +111,43 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
section: "VencordSettings",
|
section: "VencordSettings",
|
||||||
label: "Vencord",
|
label: "Vencord",
|
||||||
element: require("@components/VencordSettings/VencordTab").default,
|
element: VencordTab,
|
||||||
className: "vc-settings"
|
className: "vc-settings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
section: "VencordPlugins",
|
section: "VencordPlugins",
|
||||||
label: "Plugins",
|
label: "Plugins",
|
||||||
element: require("@components/VencordSettings/PluginsTab").default,
|
element: PluginsTab,
|
||||||
className: "vc-plugins"
|
className: "vc-plugins"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
section: "VencordThemes",
|
section: "VencordThemes",
|
||||||
label: "Themes",
|
label: "Themes",
|
||||||
element: require("@components/VencordSettings/ThemesTab").default,
|
element: ThemesTab,
|
||||||
className: "vc-themes"
|
className: "vc-themes"
|
||||||
},
|
},
|
||||||
!IS_UPDATER_DISABLED && {
|
!IS_UPDATER_DISABLED && {
|
||||||
section: "VencordUpdater",
|
section: "VencordUpdater",
|
||||||
label: "Updater",
|
label: "Updater",
|
||||||
element: require("@components/VencordSettings/UpdaterTab").default,
|
element: UpdaterTab,
|
||||||
className: "vc-updater"
|
className: "vc-updater"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
section: "VencordCloud",
|
section: "VencordCloud",
|
||||||
label: "Cloud",
|
label: "Cloud",
|
||||||
element: require("@components/VencordSettings/CloudTab").default,
|
element: CloudTab,
|
||||||
className: "vc-cloud"
|
className: "vc-cloud"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
section: "VencordSettingsSync",
|
section: "VencordSettingsSync",
|
||||||
label: "Backup & Restore",
|
label: "Backup & Restore",
|
||||||
element: require("@components/VencordSettings/BackupAndRestoreTab").default,
|
element: BackupAndRestoreTab,
|
||||||
className: "vc-backup-restore"
|
className: "vc-backup-restore"
|
||||||
},
|
},
|
||||||
IS_DEV && {
|
IS_DEV && {
|
||||||
section: "VencordPatchHelper",
|
section: "VencordPatchHelper",
|
||||||
label: "Patch Helper",
|
label: "Patch Helper",
|
||||||
element: require("@components/VencordSettings/PatchHelperTab").default,
|
element: PatchHelperTab,
|
||||||
className: "vc-patch-helper"
|
className: "vc-patch-helper"
|
||||||
},
|
},
|
||||||
...this.customSections.map(func => func(SectionTypes)),
|
...this.customSections.map(func => func(SectionTypes)),
|
||||||
|
@ -123,19 +157,63 @@ export default definePlugin({
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isRightSpot({ header, settings }: { header?: string; settings?: string[]; }) {
|
||||||
|
const firstChild = settings?.[0];
|
||||||
|
// lowest two elements... sanity backup
|
||||||
|
if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true;
|
||||||
|
|
||||||
|
const { settingsLocation } = Settings.plugins.Settings;
|
||||||
|
|
||||||
|
if (settingsLocation === "bottom") return firstChild === "LOGOUT";
|
||||||
|
if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG";
|
||||||
|
|
||||||
|
if (!header) return;
|
||||||
|
|
||||||
|
const names = {
|
||||||
|
top: i18n.Messages.USER_SETTINGS,
|
||||||
|
aboveNitro: i18n.Messages.BILLING_SETTINGS,
|
||||||
|
belowNitro: i18n.Messages.APP_SETTINGS,
|
||||||
|
aboveActivity: i18n.Messages.ACTIVITY_SETTINGS
|
||||||
|
};
|
||||||
|
return header === names[settingsLocation];
|
||||||
|
},
|
||||||
|
|
||||||
|
patchedSettings: new WeakSet(),
|
||||||
|
|
||||||
|
addSettings(elements: any[], element: { header?: string; settings: string[]; }, sectionTypes: SectionTypes) {
|
||||||
|
if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) return;
|
||||||
|
|
||||||
|
this.patchedSettings.add(elements);
|
||||||
|
|
||||||
|
elements.push(...this.makeSettingsCategories(sectionTypes));
|
||||||
|
},
|
||||||
|
|
||||||
|
wrapSettingsHook(originalHook: (...args: any[]) => Record<string, unknown>[]) {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
const elements = originalHook(...args);
|
||||||
|
if (!this.patchedSettings.has(elements))
|
||||||
|
elements.unshift(...this.makeSettingsCategories({
|
||||||
|
HEADER: "HEADER",
|
||||||
|
DIVIDER: "DIVIDER",
|
||||||
|
CUSTOM: "CUSTOM"
|
||||||
|
}));
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
settingsLocation: {
|
settingsLocation: {
|
||||||
type: OptionType.SELECT,
|
type: OptionType.SELECT,
|
||||||
description: "Where to put the Vencord settings section",
|
description: "Where to put the Vencord settings section",
|
||||||
options: [
|
options: [
|
||||||
{ label: "At the very top", value: "top" },
|
{ label: "At the very top", value: "top" },
|
||||||
{ label: "Above the Nitro section", value: "aboveNitro" },
|
{ label: "Above the Nitro section", value: "aboveNitro", default: true },
|
||||||
{ label: "Below the Nitro section", value: "belowNitro" },
|
{ label: "Below the Nitro section", value: "belowNitro" },
|
||||||
{ label: "Above Activity Settings", value: "aboveActivity", default: true },
|
{ label: "Above Activity Settings", value: "aboveActivity" },
|
||||||
{ label: "Below Activity Settings", value: "belowActivity" },
|
{ label: "Below Activity Settings", value: "belowActivity" },
|
||||||
{ label: "At the very bottom", value: "bottom" },
|
{ label: "At the very bottom", value: "bottom" },
|
||||||
],
|
]
|
||||||
restartNeeded: true
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -162,15 +240,24 @@ export default definePlugin({
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
|
||||||
makeInfoElements(Component: React.ComponentType<React.PropsWithChildren>, props: React.PropsWithChildren) {
|
getInfoRows() {
|
||||||
const { electronVersion, chromiumVersion, additionalInfo } = this;
|
const { electronVersion, chromiumVersion, additionalInfo } = this;
|
||||||
|
|
||||||
return (
|
const rows = [`Vencord ${gitHash}${additionalInfo}`];
|
||||||
<>
|
|
||||||
<Component {...props}>Vencord {gitHash}{additionalInfo}</Component>
|
if (electronVersion) rows.push(`Electron ${electronVersion}`);
|
||||||
{electronVersion && <Component {...props}>Electron {electronVersion}</Component>}
|
if (chromiumVersion) rows.push(`Chromium ${chromiumVersion}`);
|
||||||
{chromiumVersion && <Component {...props}>Chromium {chromiumVersion}</Component>}
|
|
||||||
</>
|
return rows;
|
||||||
|
},
|
||||||
|
|
||||||
|
getInfoString() {
|
||||||
|
return "\n" + this.getInfoRows().join("\n");
|
||||||
|
},
|
||||||
|
|
||||||
|
makeInfoElements(Component: React.ComponentType<React.PropsWithChildren>, props: React.PropsWithChildren) {
|
||||||
|
return this.getInfoRows().map((text, i) =>
|
||||||
|
<Component key={i} {...props}>{text}</Component>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,20 +16,24 @@
|
||||||
* 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 { DataStore } from "@api/index";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Link } from "@components/Link";
|
||||||
|
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
||||||
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { isPluginDev } from "@utils/misc";
|
import { isPluginDev } from "@utils/misc";
|
||||||
|
import { relaunch } from "@utils/native";
|
||||||
import { makeCodeblock } from "@utils/text";
|
import { makeCodeblock } from "@utils/text";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { isOutdated } from "@utils/updater";
|
import { isOutdated, update } from "@utils/updater";
|
||||||
import { Alerts, Forms, UserStore } from "@webpack/common";
|
import { Alerts, Card, ChannelStore, Forms, GuildMemberStore, NavigationRouter, Parser, RelationshipStore, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
import plugins from "~plugins";
|
import plugins from "~plugins";
|
||||||
|
|
||||||
import settings from "./settings";
|
import settings from "./settings";
|
||||||
|
|
||||||
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
|
const VENCORD_GUILD_ID = "1015060230222131221";
|
||||||
|
|
||||||
const AllowedChannelIds = [
|
const AllowedChannelIds = [
|
||||||
SUPPORT_CHANNEL_ID,
|
SUPPORT_CHANNEL_ID,
|
||||||
|
@ -37,6 +41,12 @@ const AllowedChannelIds = [
|
||||||
"1033680203433660458", // Vencord > #v
|
"1033680203433660458", // Vencord > #v
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const TrustedRolesIds = [
|
||||||
|
"1026534353167208489", // contributor
|
||||||
|
"1026504932959977532", // regular
|
||||||
|
"1042507929485586532", // donor
|
||||||
|
];
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "SupportHelper",
|
name: "SupportHelper",
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -44,6 +54,14 @@ export default definePlugin({
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
dependencies: ["CommandsAPI"],
|
dependencies: ["CommandsAPI"],
|
||||||
|
|
||||||
|
patches: [{
|
||||||
|
find: ".BEGINNING_DM.format",
|
||||||
|
replacement: {
|
||||||
|
match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
|
||||||
|
replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
|
||||||
commands: [{
|
commands: [{
|
||||||
name: "vencord-debug",
|
name: "vencord-debug",
|
||||||
description: "Send Vencord Debug info",
|
description: "Send Vencord Debug info",
|
||||||
|
@ -64,15 +82,13 @@ export default definePlugin({
|
||||||
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
|
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
|
||||||
|
|
||||||
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
|
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
|
||||||
const enabledApiPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && isApiPlugin(p));
|
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
Vencord: `v${VERSION} • ${gitHash}${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
Vencord:
|
||||||
"Discord Branch": RELEASE_CHANNEL,
|
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
|
||||||
Client: client,
|
`${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
||||||
Platform: window.navigator.platform,
|
Client: `${RELEASE_CHANNEL} ~ ${client}`,
|
||||||
Outdated: isOutdated,
|
Platform: window.navigator.platform
|
||||||
OpenAsar: "openasar" in window,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IS_DISCORD_DESKTOP) {
|
if (IS_DISCORD_DESKTOP) {
|
||||||
|
@ -80,11 +96,10 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
|
|
||||||
const debugInfo = `
|
const debugInfo = `
|
||||||
**Vencord Debug Info**
|
>>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join("\n")}
|
||||||
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
|
|
||||||
|
|
||||||
Enabled Plugins (${enabledPlugins.length + enabledApiPlugins.length}):
|
Enabled Plugins (${enabledPlugins.length}):
|
||||||
${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "))}
|
${makeCodeblock(enabledPlugins.join(", "))}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -97,24 +112,75 @@ ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "
|
||||||
async CHANNEL_SELECT({ channelId }) {
|
async CHANNEL_SELECT({ channelId }) {
|
||||||
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
||||||
|
|
||||||
if (isPluginDev(UserStore.getCurrentUser().id)) return;
|
const selfId = UserStore.getCurrentUser()?.id;
|
||||||
|
if (!selfId || isPluginDev(selfId)) return;
|
||||||
|
|
||||||
if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) {
|
if (isOutdated) {
|
||||||
const rememberDismiss = () => DataStore.set(REMEMBER_DISMISS_KEY, gitHash);
|
return Alerts.show({
|
||||||
|
|
||||||
Alerts.show({
|
|
||||||
title: "Hold on!",
|
title: "Hold on!",
|
||||||
body: <div>
|
body: <div>
|
||||||
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
|
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
|
||||||
<Forms.FormText>
|
<Forms.FormText className={Margins.top8}>
|
||||||
Please first update using the Updater Page in Settings, or use the VencordInstaller (Update Vencord Button)
|
Please first update before asking for support!
|
||||||
to do so, in case you can't access the Updater page.
|
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
</div>,
|
</div>,
|
||||||
onCancel: rememberDismiss,
|
onCancel: () => openUpdaterModal!(),
|
||||||
onConfirm: rememberDismiss
|
cancelText: "View Updates",
|
||||||
|
confirmText: "Update & Restart Now",
|
||||||
|
async onConfirm() {
|
||||||
|
await update();
|
||||||
|
relaunch();
|
||||||
|
},
|
||||||
|
secondaryConfirmText: "I know what I'm doing or I can't update"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore outdated type
|
||||||
|
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
|
||||||
|
if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
|
||||||
|
|
||||||
|
if (!IS_WEB && IS_UPDATER_DISABLED) {
|
||||||
|
return Alerts.show({
|
||||||
|
title: "Hold on!",
|
||||||
|
body: <div>
|
||||||
|
<Forms.FormText>You are using an externally updated Vencord version, which we do not provide support for!</Forms.FormText>
|
||||||
|
<Forms.FormText className={Margins.top8}>
|
||||||
|
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
|
||||||
|
contact your package maintainer for support instead.
|
||||||
|
</Forms.FormText>
|
||||||
|
</div>,
|
||||||
|
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = await VencordNative.updater.getRepo();
|
||||||
|
if (repo.ok && !repo.value.includes("Vendicated/Vencord")) {
|
||||||
|
return Alerts.show({
|
||||||
|
title: "Hold on!",
|
||||||
|
body: <div>
|
||||||
|
<Forms.FormText>You are using a fork of Vencord, which we do not provide support for!</Forms.FormText>
|
||||||
|
<Forms.FormText className={Margins.top8}>
|
||||||
|
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
|
||||||
|
contact your package maintainer for support instead.
|
||||||
|
</Forms.FormText>
|
||||||
|
</div>,
|
||||||
|
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
|
||||||
|
if (!isPluginDev(userId)) return null;
|
||||||
|
if (RelationshipStore.isFriend(userId)) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
|
||||||
|
Please do not private message Vencord plugin developers for support!
|
||||||
|
<br />
|
||||||
|
Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}
|
||||||
|
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
|
@ -73,13 +73,13 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: "instantBatchUpload:function",
|
find: "instantBatchUpload:function",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /uploadFiles:(.{1,2}),/,
|
match: /uploadFiles:(\i),/,
|
||||||
replace:
|
replace:
|
||||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
|
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "message.attachments",
|
find: 'addFilesTo:"message.attachments"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(\i.uploadFiles\((\i),)/,
|
match: /(\i.uploadFiles\((\i),)/,
|
||||||
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"
|
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"
|
||||||
|
|
5
src/plugins/automodContext/README.md
Normal file
5
src/plugins/automodContext/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# AutomodContext
|
||||||
|
|
||||||
|
Allows you to jump to the messages surrounding an automod hit
|
||||||
|
|
||||||
|

|
73
src/plugins/automodContext/index.tsx
Normal file
73
src/plugins/automodContext/index.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { Button, ChannelStore, Text } from "@webpack/common";
|
||||||
|
|
||||||
|
const { selectChannel } = findByPropsLazy("selectChannel", "selectVoiceChannel");
|
||||||
|
|
||||||
|
function jumpToMessage(channelId: string, messageId: string) {
|
||||||
|
const guildId = ChannelStore.getChannel(channelId)?.guild_id;
|
||||||
|
|
||||||
|
selectChannel({
|
||||||
|
guildId,
|
||||||
|
channelId,
|
||||||
|
messageId,
|
||||||
|
jumpType: "INSTANT"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findChannelId(message: any): string | null {
|
||||||
|
const { embeds: [embed] } = message;
|
||||||
|
const channelField = embed.fields.find(({ rawName }) => rawName === "channel_id");
|
||||||
|
|
||||||
|
if (!channelField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelField.rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "AutomodContext",
|
||||||
|
description: "Allows you to jump to the messages surrounding an automod hit.",
|
||||||
|
authors: [Devs.JohnyTheCarrot],
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".Messages.GUILD_AUTOMOD_REPORT_ISSUES",
|
||||||
|
replacement: {
|
||||||
|
match: /\.Messages\.ACTIONS.+?}\)(?=,(\(0.{0,40}\.dot.*?}\)),)/,
|
||||||
|
replace: (m, dot) => `${m},${dot},$self.renderJumpButton({message:arguments[0].message})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
renderJumpButton: ErrorBoundary.wrap(({ message }: { message: any; }) => {
|
||||||
|
const channelId = findChannelId(message);
|
||||||
|
|
||||||
|
if (!channelId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
style={{ padding: "2px 8px" }}
|
||||||
|
look={Button.Looks.LINK}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
color={Button.Colors.LINK}
|
||||||
|
onClick={() => jumpToMessage(channelId, message.id)}
|
||||||
|
>
|
||||||
|
<Text color="text-link" variant="text-xs/normal">
|
||||||
|
Jump to Surrounding
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}, { noop: true })
|
||||||
|
});
|
|
@ -20,7 +20,7 @@ import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
import { FluxDispatcher, i18n } from "@webpack/common";
|
import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
|
||||||
|
|
||||||
import FolderSideBar from "./FolderSideBar";
|
import FolderSideBar from "./FolderSideBar";
|
||||||
|
|
||||||
|
@ -117,8 +117,8 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
|
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
|
||||||
{
|
{
|
||||||
match: /(useStateFromStoresArray\).{0,25}let \i)=(\i\.\i.getGuildsTree\(\))/,
|
match: /\[(\i)\]=(\(0,\i\.useStateFromStoresArray\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
|
||||||
replace: (_, rest, guildsTree) => `${rest}=$self.getGuildTree(!!arguments[0].isBetterFolders,${guildsTree},arguments[0].betterFoldersExpandedIds)`
|
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0].isBetterFolders,betterFoldersOriginalTree,arguments[0].betterFoldersExpandedIds)`
|
||||||
},
|
},
|
||||||
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
|
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
|
||||||
{
|
{
|
||||||
|
@ -127,7 +127,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
|
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
|
||||||
{
|
{
|
||||||
match: /unreadMentionsIndicatorBottom,barClassName.+?}\)\]/,
|
match: /unreadMentionsIndicatorBottom,.+?}\)\]/,
|
||||||
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
|
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
|
||||||
},
|
},
|
||||||
// Export the isBetterFolders variable to the folders component
|
// Export the isBetterFolders variable to the folders component
|
||||||
|
@ -209,7 +209,7 @@ export default definePlugin({
|
||||||
predicate: () => settings.store.closeAllHomeButton,
|
predicate: () => settings.store.closeAllHomeButton,
|
||||||
replacement: {
|
replacement: {
|
||||||
// Close all folders when clicking the home button
|
// Close all folders when clicking the home button
|
||||||
match: /(?<=onClick:\(\)=>{)(?=.{0,200}"discodo")/,
|
match: /(?<=onClick:\(\)=>{)(?=.{0,300}"discodo")/,
|
||||||
replace: "$self.closeFolders();"
|
replace: "$self.closeFolders();"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,19 +252,21 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getGuildTree(isBetterFolders: boolean, oldTree: any, expandedFolderIds?: Set<any>) {
|
getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set<any>) {
|
||||||
if (!isBetterFolders || expandedFolderIds == null) return oldTree;
|
return useMemo(() => {
|
||||||
|
if (!isBetterFolders || expandedFolderIds == null) return originalTree;
|
||||||
|
|
||||||
const newTree = new GuildsTree();
|
const newTree = new GuildsTree();
|
||||||
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders
|
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders
|
||||||
newTree.root.children = oldTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
|
newTree.root.children = originalTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
|
||||||
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
|
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
|
||||||
newTree.nodes = Object.fromEntries(
|
newTree.nodes = Object.fromEntries(
|
||||||
Object.entries(oldTree.nodes)
|
Object.entries(originalTree.nodes)
|
||||||
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
|
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
|
||||||
);
|
);
|
||||||
|
|
||||||
return newTree;
|
return newTree;
|
||||||
|
}, [isBetterFolders, originalTree, expandedFolderIds]);
|
||||||
},
|
},
|
||||||
|
|
||||||
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
|
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
|
||||||
|
@ -279,7 +281,7 @@ export default definePlugin({
|
||||||
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
|
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
|
||||||
return child => {
|
return child => {
|
||||||
if (isBetterFolders) {
|
if (isBetterFolders) {
|
||||||
return "onScroll" in child.props;
|
return child?.props?.onScroll != null;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { canonicalizeMatch } from "@utils/patches";
|
import { canonicalizeMatch } from "@utils/patches";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
@ -60,7 +61,7 @@ export default definePlugin({
|
||||||
find: ".popularApplicationCommandIds,",
|
find: ".popularApplicationCommandIds,",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /lastSection:(!?\i)}\),/,
|
match: /lastSection:(!?\i)}\),/,
|
||||||
replace: "$&$self.patchPadding($1),"
|
replace: "$&$self.patchPadding({lastSection:$1}),"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -80,10 +81,10 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
patchPadding(lastSection: any) {
|
patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
|
||||||
if (!lastSection) return;
|
if (!lastSection) return null;
|
||||||
return (
|
return (
|
||||||
<div className={UserPopoutSectionCssClasses.lastSection}></div>
|
<div className={UserPopoutSectionCssClasses.lastSection} ></div>
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# BetterRoleContext
|
# BetterRoleContext
|
||||||
|
|
||||||
Adds options to copy role color and edit role when right clicking roles in the user profile
|
Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,11 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { ImageIcon } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { getCurrentGuild } from "@utils/discord";
|
import { getCurrentGuild, openImageModal } from "@utils/discord";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
|
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
|
||||||
|
|
||||||
|
@ -34,10 +36,34 @@ function AppearanceIcon() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
roleIconFileFormat: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "File format to use when viewing role icons",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "png",
|
||||||
|
value: "png",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "webp",
|
||||||
|
value: "webp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "jpg",
|
||||||
|
value: "jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BetterRoleContext",
|
name: "BetterRoleContext",
|
||||||
description: "Adds options to copy role color / edit role when right clicking roles in the user profile",
|
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven, Devs.goodbee],
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
// DeveloperMode needs to be enabled for the context menu to be shown
|
// DeveloperMode needs to be enabled for the context menu to be shown
|
||||||
|
@ -63,6 +89,20 @@ export default definePlugin({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (role.icon) {
|
||||||
|
children.push(
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-view-role-icon"
|
||||||
|
label="View Role Icon"
|
||||||
|
action={() => {
|
||||||
|
openImageModal(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`);
|
||||||
|
}}
|
||||||
|
icon={ImageIcon}
|
||||||
|
/>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
|
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
|
||||||
children.push(
|
children.push(
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
|
|
|
@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
|
||||||
import { React, RestAPI, Tooltip } from "@webpack/common";
|
import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
import { RenameButton } from "./components/RenameButton";
|
import { RenameButton } from "./components/RenameButton";
|
||||||
import { Session, SessionInfo } from "./types";
|
import { Session, SessionInfo } from "./types";
|
||||||
|
@ -168,7 +168,7 @@ export default definePlugin({
|
||||||
|
|
||||||
async checkNewSessions() {
|
async checkNewSessions() {
|
||||||
const data = await RestAPI.get({
|
const data = await RestAPI.get({
|
||||||
url: "/auth/sessions"
|
url: Constants.Endpoints.AUTH_SESSIONS
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const session of data.body.user_sessions) {
|
for (const session of data.body.user_sessions) {
|
||||||
|
|
|
@ -6,17 +6,18 @@
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { waitFor } from "@webpack";
|
||||||
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
|
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
|
||||||
import type { HTMLAttributes, ReactElement } from "react";
|
import type { HTMLAttributes, ReactElement } from "react";
|
||||||
|
|
||||||
type SettingsEntry = { section: string, label: string; };
|
type SettingsEntry = { section: string, label: string; };
|
||||||
|
|
||||||
const cl = classNameFactory("");
|
const cl = classNameFactory("");
|
||||||
const Classes = findByPropsLazy("animating", "baseLayer", "bg", "layer", "layers");
|
let Classes: Record<string, string>;
|
||||||
|
waitFor(["animating", "baseLayer", "bg", "layer", "layers"], m => Classes = m);
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
disableFade: {
|
disableFade: {
|
||||||
|
@ -118,18 +119,25 @@ export default definePlugin({
|
||||||
{ // Settings cog context menu
|
{ // Settings cog context menu
|
||||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\(0,\i.default\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
match: /\(0,\i.useDefaultUserSettingsSections\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
||||||
replace: "$self.wrapMenu($&)"
|
replace: "$self.wrapMenu($&)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
|
||||||
|
// without possibly also catching unrelated errors of children.
|
||||||
|
//
|
||||||
|
// Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
|
||||||
|
// try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
|
||||||
|
// not in children
|
||||||
Layer(props: LayerProps) {
|
Layer(props: LayerProps) {
|
||||||
return (
|
if (!FocusLock || !ComponentDispatch || !Classes) {
|
||||||
<ErrorBoundary fallback={() => props.children as any}>
|
new Logger("BetterSettings").error("Failed to find some components");
|
||||||
<Layer {...props} />
|
return props.children;
|
||||||
</ErrorBoundary>
|
}
|
||||||
);
|
|
||||||
|
return <Layer {...props} />;
|
||||||
},
|
},
|
||||||
|
|
||||||
wrapMenu(list: SettingsEntry[]) {
|
wrapMenu(list: SettingsEntry[]) {
|
||||||
|
|
|
@ -24,22 +24,20 @@ import { closeAllModals } from "@utils/modal";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { maybePromptToUpdate } from "@utils/updater";
|
import { maybePromptToUpdate } from "@utils/updater";
|
||||||
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
|
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
|
||||||
import { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
|
import { DraftType, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
|
||||||
|
|
||||||
const CrashHandlerLogger = new Logger("CrashHandler");
|
const CrashHandlerLogger = new Logger("CrashHandler");
|
||||||
const { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => {
|
|
||||||
const modules = findBulk(
|
const { ModalStack, DraftManager, closeExpressionPicker } = proxyLazyWebpack(() => {
|
||||||
|
const [ModalStack, DraftManager, ExpressionManager] = findBulk(
|
||||||
filters.byProps("pushLazy", "popAll"),
|
filters.byProps("pushLazy", "popAll"),
|
||||||
filters.byProps("clearDraft", "saveDraft"),
|
filters.byProps("clearDraft", "saveDraft"),
|
||||||
filters.byProps("DraftType"),
|
filters.byProps("closeExpressionPicker", "openExpressionPicker"),);
|
||||||
filters.byProps("closeExpressionPicker", "openExpressionPicker"),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ModalStack: modules[0],
|
ModalStack,
|
||||||
DraftManager: modules[1],
|
DraftManager,
|
||||||
DraftType: modules[2]?.DraftType,
|
closeExpressionPicker: ExpressionManager?.closeExpressionPicker,
|
||||||
closeExpressionPicker: modules[3]?.closeExpressionPicker,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -104,7 +102,7 @@ export default definePlugin({
|
||||||
|
|
||||||
shouldAttemptRecover = false;
|
shouldAttemptRecover = false;
|
||||||
// This is enough to avoid a crash loop
|
// This is enough to avoid a crash loop
|
||||||
setTimeout(() => shouldAttemptRecover = true, 500);
|
setTimeout(() => shouldAttemptRecover = true, 1000);
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -137,8 +135,11 @@ export default definePlugin({
|
||||||
try {
|
try {
|
||||||
const channelId = SelectedChannelStore.getChannelId();
|
const channelId = SelectedChannelStore.getChannelId();
|
||||||
|
|
||||||
DraftManager.clearDraft(channelId, DraftType.ChannelMessage);
|
for (const key in DraftType) {
|
||||||
DraftManager.clearDraft(channelId, DraftType.FirstThreadMessage);
|
if (!Number.isNaN(Number(key))) continue;
|
||||||
|
|
||||||
|
DraftManager.clearDraft(channelId, DraftType[key]);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
CrashHandlerLogger.debug("Failed to clear drafts.", err);
|
CrashHandlerLogger.debug("Failed to clear drafts.", err);
|
||||||
}
|
}
|
||||||
|
|
68
src/plugins/ctrlEnterSend/index.ts
Normal file
68
src/plugins/ctrlEnterSend/index.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "CtrlEnterSend",
|
||||||
|
authors: [Devs.UlyssesZhan],
|
||||||
|
description: "Use Ctrl+Enter to send messages (customizable)",
|
||||||
|
settings: definePluginSettings({
|
||||||
|
submitRule: {
|
||||||
|
description: "The way to send a message",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Ctrl+Enter (Enter or Shift+Enter for new line)",
|
||||||
|
value: "ctrl+enter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Shift+Enter (Enter for new line)",
|
||||||
|
value: "shift+enter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Enter (Shift+Enter for new line; Discord default)",
|
||||||
|
value: "enter"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
default: "ctrl+enter"
|
||||||
|
},
|
||||||
|
sendMessageInTheMiddleOfACodeBlock: {
|
||||||
|
description: "Whether to send a message in the middle of a code block",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "KeyboardKeys.ENTER&&(!",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=(\i)\.which===\i\.KeyboardKeys.ENTER&&).{0,100}(\(0,\i\.hasOpenPlainTextCodeBlock\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
|
||||||
|
replace: "$self.shouldSubmit($1, $2)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
shouldSubmit(event: KeyboardEvent, codeblock: boolean): boolean {
|
||||||
|
let result = false;
|
||||||
|
switch (this.settings.store.submitRule) {
|
||||||
|
case "shift+enter":
|
||||||
|
result = event.shiftKey;
|
||||||
|
break;
|
||||||
|
case "ctrl+enter":
|
||||||
|
result = event.ctrlKey;
|
||||||
|
break;
|
||||||
|
case "enter":
|
||||||
|
result = !event.shiftKey && !event.ctrlKey;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!this.settings.store.sendMessageInTheMiddleOfACodeBlock) {
|
||||||
|
result &&= !codeblock;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
|
@ -17,13 +17,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings, Settings } from "@api/Settings";
|
import { definePluginSettings, Settings } from "@api/Settings";
|
||||||
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { isTruthy } from "@utils/guards";
|
import { isTruthy } from "@utils/guards";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||||
import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
|
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, StatusSettingsStores, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
|
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
|
||||||
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
|
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
|
||||||
|
@ -386,17 +389,36 @@ async function setRpc(disable?: boolean) {
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "CustomRPC",
|
name: "CustomRPC",
|
||||||
description: "Allows you to set a custom rich presence.",
|
description: "Allows you to set a custom rich presence.",
|
||||||
authors: [Devs.captain, Devs.AutumnVN],
|
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
|
||||||
start: setRpc,
|
start: setRpc,
|
||||||
stop: () => setRpc(true),
|
stop: () => setRpc(true),
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
settingsAboutComponent: () => {
|
settingsAboutComponent: () => {
|
||||||
const activity = useAwaiter(createActivity);
|
const activity = useAwaiter(createActivity);
|
||||||
|
const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
|
||||||
const { profileThemeStyle } = useProfileThemeStyle({});
|
const { profileThemeStyle } = useProfileThemeStyle({});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{!gameActivityEnabled && (
|
||||||
|
<ErrorCard
|
||||||
|
className={classes(Margins.top16, Margins.bottom16)}
|
||||||
|
style={{ padding: "1em" }}
|
||||||
|
>
|
||||||
|
<Forms.FormTitle>Notice</Forms.FormTitle>
|
||||||
|
<Forms.FormText>Game activity isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color={Button.Colors.TRANSPARENT}
|
||||||
|
className={Margins.top8}
|
||||||
|
onClick={() => StatusSettingsStores.ShowCurrentGame.updateSetting(true)}
|
||||||
|
>
|
||||||
|
Enable
|
||||||
|
</Button>
|
||||||
|
</ErrorCard>
|
||||||
|
)}
|
||||||
|
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
|
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
|
||||||
get the application ID.
|
get the application ID.
|
||||||
|
@ -407,7 +429,9 @@ export default definePlugin({
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address".
|
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address".
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Forms.FormDivider />
|
|
||||||
|
<Forms.FormDivider className={Margins.top8} />
|
||||||
|
|
||||||
<div style={{ width: "284px", ...profileThemeStyle }}>
|
<div style={{ width: "284px", ...profileThemeStyle }}>
|
||||||
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
|
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
|
||||||
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
|
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
|
||||||
|
|
5
src/plugins/customidle/README.md
Normal file
5
src/plugins/customidle/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# CustomIdle
|
||||||
|
|
||||||
|
Lets you change the time until your status gets automatically set to idle. You can also prevent idling altogether.
|
||||||
|
|
||||||
|

|
94
src/plugins/customidle/index.ts
Normal file
94
src/plugins/customidle/index.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Notices } from "@api/index";
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { makeRange } from "@components/PluginSettings/components";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { FluxDispatcher } from "@webpack/common";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
idleTimeout: {
|
||||||
|
description: "Minutes before Discord goes idle (0 to disable auto-idle)",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
markers: makeRange(0, 60, 5),
|
||||||
|
default: 10,
|
||||||
|
stickToMarkers: false,
|
||||||
|
restartNeeded: true // Because of the setInterval patch
|
||||||
|
},
|
||||||
|
remainInIdle: {
|
||||||
|
description: "When you come back to Discord, remain idle until you confirm you want to go online",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "CustomIdle",
|
||||||
|
description: "Allows you to set the time before Discord goes idle (or disable auto-idle)",
|
||||||
|
authors: [Devs.newwares],
|
||||||
|
settings,
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "IDLE_DURATION:function(){return",
|
||||||
|
replacement: {
|
||||||
|
match: /(IDLE_DURATION:function\(\){return )\i/,
|
||||||
|
replace: "$1$self.getIdleTimeout()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: 'type:"IDLE",idle:',
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /Math\.min\((\i\.AfkTimeout\.getSetting\(\)\*\i\.default\.Millis\.SECOND),\i\.IDLE_DURATION\)/,
|
||||||
|
replace: "$1" // Decouple idle from afk (phone notifications will remain at user setting or 10 min maximum)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /\i\.default\.dispatch\({type:"IDLE",idle:!1}\)/,
|
||||||
|
replace: "$self.handleOnline()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(setInterval\(\i,\.25\*)\i\.IDLE_DURATION/,
|
||||||
|
replace: "$1$self.getIntervalDelay()" // For web installs
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
getIntervalDelay() {
|
||||||
|
return Math.min(6e5, this.getIdleTimeout());
|
||||||
|
},
|
||||||
|
|
||||||
|
handleOnline() {
|
||||||
|
if (!settings.store.remainInIdle) {
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "IDLE",
|
||||||
|
idle: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backOnlineMessage = "Welcome back! Click the button to go online. Click the X to stay idle until reload.";
|
||||||
|
if (
|
||||||
|
Notices.currentNotice?.[1] === backOnlineMessage ||
|
||||||
|
Notices.noticesQueue.some(([, noticeMessage]) => noticeMessage === backOnlineMessage)
|
||||||
|
) return;
|
||||||
|
|
||||||
|
Notices.showNotice(backOnlineMessage, "Exit idle", () => {
|
||||||
|
Notices.popNotice();
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "IDLE",
|
||||||
|
idle: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getIdleTimeout() { // milliseconds, default is 6e5
|
||||||
|
const { idleTimeout } = settings.store;
|
||||||
|
return idleTimeout === 0 ? Infinity : idleTimeout * 60000;
|
||||||
|
}
|
||||||
|
});
|
|
@ -6,10 +6,11 @@
|
||||||
|
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { Tooltip } from "@webpack/common";
|
import { Tooltip } from "@webpack/common";
|
||||||
import type { Component } from "react";
|
import type { Component } from "react";
|
||||||
|
|
||||||
|
@ -34,11 +35,19 @@ interface Props {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enum ReplaceElements {
|
||||||
|
ReplaceAllElements,
|
||||||
|
ReplaceTitlesOnly,
|
||||||
|
ReplaceThumbnailsOnly
|
||||||
|
}
|
||||||
|
|
||||||
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
|
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
|
||||||
|
|
||||||
async function embedDidMount(this: Component<Props>) {
|
async function embedDidMount(this: Component<Props>) {
|
||||||
try {
|
try {
|
||||||
const { embed } = this.props;
|
const { embed } = this.props;
|
||||||
|
const { replaceElements } = settings.store;
|
||||||
|
|
||||||
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
|
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
|
||||||
|
|
||||||
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
|
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
|
||||||
|
@ -58,12 +67,12 @@ async function embedDidMount(this: Component<Props>) {
|
||||||
enabled: true
|
enabled: true
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasTitle) {
|
if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
|
||||||
embed.dearrow.oldTitle = embed.rawTitle;
|
embed.dearrow.oldTitle = embed.rawTitle;
|
||||||
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
|
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasThumb) {
|
if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
|
||||||
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||||
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
|
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
|
||||||
}
|
}
|
||||||
|
@ -128,10 +137,30 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
hideButton: {
|
||||||
|
description: "Hides the Dearrow button from YouTube embeds",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false,
|
||||||
|
restartNeeded: true
|
||||||
|
},
|
||||||
|
replaceElements: {
|
||||||
|
description: "Choose which elements of the embed will be replaced",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
restartNeeded: true,
|
||||||
|
options: [
|
||||||
|
{ label: "Everything (Titles & Thumbnails)", value: ReplaceElements.ReplaceAllElements, default: true },
|
||||||
|
{ label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
|
||||||
|
{ label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Dearrow",
|
name: "Dearrow",
|
||||||
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
|
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
|
settings,
|
||||||
|
|
||||||
embedDidMount,
|
embedDidMount,
|
||||||
renderButton(component: Component<Props>) {
|
renderButton(component: Component<Props>) {
|
||||||
|
@ -154,7 +183,8 @@ export default definePlugin({
|
||||||
// add dearrow button
|
// add dearrow button
|
||||||
{
|
{
|
||||||
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
|
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
|
||||||
replace: "children:[$self.renderButton(this),"
|
replace: "children:[$self.renderButton(this),",
|
||||||
|
predicate: () => !settings.store.hideButton
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { proxyLazy } from "@utils/lazy";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { openModal } from "@utils/modal";
|
import { openModal } from "@utils/modal";
|
||||||
import { OAuth2AuthorizeModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from "@webpack/common";
|
import { OAuth2AuthorizeModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from "@webpack/common";
|
||||||
import type { StateStorage } from "zustand/middleware";
|
|
||||||
|
|
||||||
import { AUTHORIZE_URL, CLIENT_ID } from "../constants";
|
import { AUTHORIZE_URL, CLIENT_ID } from "../constants";
|
||||||
|
|
||||||
|
@ -23,7 +22,7 @@ interface AuthorizationState {
|
||||||
isAuthorized: () => boolean;
|
isAuthorized: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexedDBStorage: StateStorage = {
|
const indexedDBStorage = {
|
||||||
async getItem(name: string): Promise<string | null> {
|
async getItem(name: string): Promise<string | null> {
|
||||||
return DataStore.get(name).then(v => v ?? null);
|
return DataStore.get(name).then(v => v ?? null);
|
||||||
},
|
},
|
||||||
|
@ -36,9 +35,9 @@ const indexedDBStorage: StateStorage = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Move switching accounts subscription inside the store?
|
// TODO: Move switching accounts subscription inside the store?
|
||||||
export const useAuthorizationStore = proxyLazy(() => zustandCreate<AuthorizationState>(
|
export const useAuthorizationStore = proxyLazy(() => zustandCreate(
|
||||||
zustandPersist(
|
zustandPersist(
|
||||||
(set, get) => ({
|
(set: any, get: any) => ({
|
||||||
token: null,
|
token: null,
|
||||||
tokens: {},
|
tokens: {},
|
||||||
init: () => { set({ token: get().tokens[UserStore.getCurrentUser().id] ?? null }); },
|
init: () => { set({ token: get().tokens[UserStore.getCurrentUser().id] ?? null }); },
|
||||||
|
@ -91,7 +90,7 @@ export const useAuthorizationStore = proxyLazy(() => zustandCreate<Authorization
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
isAuthorized: () => !!get().token,
|
isAuthorized: () => !!get().token,
|
||||||
}),
|
} as AuthorizationState),
|
||||||
{
|
{
|
||||||
name: "decor-auth",
|
name: "decor-auth",
|
||||||
getStorage: () => indexedDBStorage,
|
getStorage: () => indexedDBStorage,
|
||||||
|
|
|
@ -21,7 +21,7 @@ interface UserDecorationsState {
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate<UserDecorationsState>((set, get) => ({
|
export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({
|
||||||
decorations: [],
|
decorations: [],
|
||||||
selectedDecoration: null,
|
selectedDecoration: null,
|
||||||
async fetch() {
|
async fetch() {
|
||||||
|
@ -53,4 +53,4 @@ export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate<User
|
||||||
useUsersDecorationsStore.getState().set(UserStore.getCurrentUser().id, decoration ? decorationToAsset(decoration) : null);
|
useUsersDecorationsStore.getState().set(UserStore.getCurrentUser().id, decoration ? decorationToAsset(decoration) : null);
|
||||||
},
|
},
|
||||||
clear: () => set({ decorations: [], selectedDecoration: null })
|
clear: () => set({ decorations: [], selectedDecoration: null })
|
||||||
})));
|
} as UserDecorationsState)));
|
||||||
|
|
|
@ -30,7 +30,7 @@ interface UsersDecorationsState {
|
||||||
set: (userId: string, decoration: string | null) => void;
|
set: (userId: string, decoration: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecorationsState>((set, get) => ({
|
export const useUsersDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({
|
||||||
usersDecorations: new Map<string, UserDecorationData>(),
|
usersDecorations: new Map<string, UserDecorationData>(),
|
||||||
fetchQueue: new Set(),
|
fetchQueue: new Set(),
|
||||||
bulkFetch: debounce(async () => {
|
bulkFetch: debounce(async () => {
|
||||||
|
@ -40,7 +40,7 @@ export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecor
|
||||||
|
|
||||||
set({ fetchQueue: new Set() });
|
set({ fetchQueue: new Set() });
|
||||||
|
|
||||||
const fetchIds = Array.from(fetchQueue);
|
const fetchIds = [...fetchQueue];
|
||||||
const fetchedUsersDecorations = await getUsersDecorations(fetchIds);
|
const fetchedUsersDecorations = await getUsersDecorations(fetchIds);
|
||||||
|
|
||||||
const newUsersDecorations = new Map(usersDecorations);
|
const newUsersDecorations = new Map(usersDecorations);
|
||||||
|
@ -92,7 +92,7 @@ export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecor
|
||||||
newUsersDecorations.set(userId, { asset: decoration, fetchedAt: new Date() });
|
newUsersDecorations.set(userId, { asset: decoration, fetchedAt: new Date() });
|
||||||
set({ usersDecorations: newUsersDecorations });
|
set({ usersDecorations: newUsersDecorations });
|
||||||
}
|
}
|
||||||
})));
|
} as UsersDecorationsState)));
|
||||||
|
|
||||||
export function useUserDecorAvatarDecoration(user?: User): AvatarDecoration | null | undefined {
|
export function useUserDecorAvatarDecoration(user?: User): AvatarDecoration | null | undefined {
|
||||||
const [decorAvatarDecoration, setDecorAvatarDecoration] = useState<string | null>(user ? useUsersDecorationsStore.getState().getAsset(user.id) ?? null : null);
|
const [decorAvatarDecoration, setDecorAvatarDecoration] = useState<string | null>(user ? useUsersDecorationsStore.getState().getAsset(user.id) ?? null : null);
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { openChangeDecorationModal } from "../modals/ChangeDecorationModal";
|
||||||
|
|
||||||
const CustomizationSection = findByCodeLazy(".customizationSectionBackground");
|
const CustomizationSection = findByCodeLazy(".customizationSectionBackground");
|
||||||
|
|
||||||
interface DecorSectionProps {
|
export interface DecorSectionProps {
|
||||||
hideTitle?: boolean;
|
hideTitle?: boolean;
|
||||||
hideDivider?: boolean;
|
hideDivider?: boolean;
|
||||||
noMargin?: boolean;
|
noMargin?: boolean;
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { Margins } from "@utils/margins";
|
||||||
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
|
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
import { EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
|
import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
|
||||||
import { Promisable } from "type-fest";
|
import { Promisable } from "type-fest";
|
||||||
|
|
||||||
const StickersStore = findStoreLazy("StickersStore");
|
const StickersStore = findStoreLazy("StickersStore");
|
||||||
|
@ -64,7 +64,7 @@ async function fetchSticker(id: string) {
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const { body } = await RestAPI.get({
|
const { body } = await RestAPI.get({
|
||||||
url: `/stickers/${id}`
|
url: Constants.Endpoints.STICKER(id)
|
||||||
});
|
});
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
|
@ -83,7 +83,7 @@ async function cloneSticker(guildId: string, sticker: Sticker) {
|
||||||
data.append("file", await fetchBlob(getUrl(sticker)));
|
data.append("file", await fetchBlob(getUrl(sticker)));
|
||||||
|
|
||||||
const { body } = await RestAPI.post({
|
const { body } = await RestAPI.post({
|
||||||
url: `/guilds/${guildId}/stickers`,
|
url: Constants.Endpoints.GUILD_STICKER_PACKS(guildId),
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -322,8 +322,9 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
|
||||||
switch (favoriteableType) {
|
switch (favoriteableType) {
|
||||||
case "emoji":
|
case "emoji":
|
||||||
const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
|
const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
|
||||||
if (!match) return;
|
const reaction = props.message.reactions.find(reaction => reaction.emoji.id === favoriteableId);
|
||||||
const name = match[1] ?? "FakeNitroEmoji";
|
if (!match && !reaction) return;
|
||||||
|
const name = (match && match[1]) ?? reaction?.emoji.name ?? "FakeNitroEmoji";
|
||||||
|
|
||||||
return buildMenuItem("Emoji", () => ({
|
return buildMenuItem("Emoji", () => ({
|
||||||
id: favoriteableId,
|
id: favoriteableId,
|
||||||
|
|
|
@ -20,10 +20,12 @@ import { definePluginSettings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { ErrorCard } from "@components/ErrorCard";
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Forms, React } from "@webpack/common";
|
import { Forms, React, UserStore } from "@webpack/common";
|
||||||
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
|
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
|
||||||
|
|
||||||
|
@ -68,8 +70,8 @@ export default definePlugin({
|
||||||
predicate: () => settings.store.enableIsStaff,
|
predicate: () => settings.store.enableIsStaff,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /=>*?(\i)\.hasFlag\((\i\.\i)\.STAFF\)}/,
|
match: /(?<=>)(\i)\.hasFlag\((\i\.\i)\.STAFF\)(?=})/,
|
||||||
replace: (_, user, flags) => `=>Vencord.Webpack.Common.UserStore.getCurrentUser()?.id===${user}.id||${user}.hasFlag(${flags}.STAFF)}`
|
replace: (_, user, flags) => `$self.isStaff(${user},${flags})`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /hasFreePremium\(\){return this.isStaff\(\)\s*?\|\|/,
|
match: /hasFreePremium\(\){return this.isStaff\(\)\s*?\|\|/,
|
||||||
|
@ -86,6 +88,15 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
isStaff(user: User, flags: any) {
|
||||||
|
try {
|
||||||
|
return UserStore.getCurrentUser()?.id === user.id || user.hasFlag(flags.STAFF);
|
||||||
|
} catch (err) {
|
||||||
|
new Logger("Experiments").error(err);
|
||||||
|
return user.hasFlag(flags.STAFF);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
settingsAboutComponent: () => {
|
settingsAboutComponent: () => {
|
||||||
const isMacOS = navigator.platform.includes("Mac");
|
const isMacOS = navigator.platform.includes("Mac");
|
||||||
const modKey = isMacOS ? "cmd" : "ctrl";
|
const modKey = isMacOS ? "cmd" : "ctrl";
|
||||||
|
|
|
@ -24,13 +24,12 @@ import { getCurrentGuild } from "@utils/discord";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
|
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
|
||||||
import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
|
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
|
||||||
import type { CustomEmoji } from "@webpack/types";
|
import type { Emoji } from "@webpack/types";
|
||||||
import type { Message } from "discord-types/general";
|
import type { Message } from "discord-types/general";
|
||||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
|
|
||||||
const DRAFT_TYPE = 0;
|
|
||||||
const StickerStore = findStoreLazy("StickersStore") as {
|
const StickerStore = findStoreLazy("StickersStore") as {
|
||||||
getPremiumPacks(): StickerPack[];
|
getPremiumPacks(): StickerPack[];
|
||||||
getAllGuildStickers(): Map<string, Sticker[]>;
|
getAllGuildStickers(): Map<string, Sticker[]>;
|
||||||
|
@ -39,6 +38,7 @@ const StickerStore = findStoreLazy("StickersStore") as {
|
||||||
|
|
||||||
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
||||||
const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
|
const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
|
||||||
|
const RoleSubscriptionEmojiUtils = findByPropsLazy("isUnusableRoleSubscriptionEmoji");
|
||||||
|
|
||||||
function searchProtoClassField(localName: string, protoClass: any) {
|
function searchProtoClassField(localName: string, protoClass: any) {
|
||||||
const field = protoClass?.fields?.find((field: any) => field.localName === localName);
|
const field = protoClass?.fields?.find((field: any) => field.localName === localName);
|
||||||
|
@ -54,16 +54,22 @@ const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoCla
|
||||||
|
|
||||||
|
|
||||||
const enum EmojiIntentions {
|
const enum EmojiIntentions {
|
||||||
REACTION = 0,
|
REACTION,
|
||||||
STATUS = 1,
|
STATUS,
|
||||||
COMMUNITY_CONTENT = 2,
|
COMMUNITY_CONTENT,
|
||||||
CHAT = 3,
|
CHAT,
|
||||||
GUILD_STICKER_RELATED_EMOJI = 4,
|
GUILD_STICKER_RELATED_EMOJI,
|
||||||
GUILD_ROLE_BENEFIT_EMOJI = 5,
|
GUILD_ROLE_BENEFIT_EMOJI,
|
||||||
COMMUNITY_CONTENT_ONLY = 6,
|
COMMUNITY_CONTENT_ONLY,
|
||||||
SOUNDBOARD = 7
|
SOUNDBOARD,
|
||||||
|
VOICE_CHANNEL_TOPIC,
|
||||||
|
GIFT,
|
||||||
|
AUTO_SUGGESTION,
|
||||||
|
POLLS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IS_BYPASSEABLE_INTENTION = `[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`;
|
||||||
|
|
||||||
const enum StickerType {
|
const enum StickerType {
|
||||||
PNG = 1,
|
PNG = 1,
|
||||||
APNG = 2,
|
APNG = 2,
|
||||||
|
@ -111,7 +117,7 @@ const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
enableEmojiBypass: {
|
enableEmojiBypass: {
|
||||||
description: "Allow sending fake emojis",
|
description: "Allows sending fake emojis (also bypasses missing permission to use custom emojis)",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
default: true,
|
default: true,
|
||||||
restartNeeded: true
|
restartNeeded: true
|
||||||
|
@ -129,7 +135,7 @@ const settings = definePluginSettings({
|
||||||
restartNeeded: true
|
restartNeeded: true
|
||||||
},
|
},
|
||||||
enableStickerBypass: {
|
enableStickerBypass: {
|
||||||
description: "Allow sending fake stickers",
|
description: "Allows sending fake stickers (also bypasses missing permission to use stickers)",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
default: true,
|
default: true,
|
||||||
restartNeeded: true
|
restartNeeded: true
|
||||||
|
@ -166,10 +172,13 @@ const settings = definePluginSettings({
|
||||||
description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.",
|
description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.",
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
default: "{{NAME}}"
|
default: "{{NAME}}"
|
||||||
|
},
|
||||||
|
disableEmbedPermissionCheck: {
|
||||||
|
description: "Whether to disable the embed permission check when sending fake emojis and stickers",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
}).withPrivateSettings<{
|
});
|
||||||
disableEmbedPermissionCheck: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
function hasPermission(channelId: string, permission: bigint) {
|
function hasPermission(channelId: string, permission: bigint) {
|
||||||
const channel = ChannelStore.getChannel(channelId);
|
const channel = ChannelStore.getChannel(channelId);
|
||||||
|
@ -187,7 +196,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FakeNitro",
|
name: "FakeNitro",
|
||||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
|
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
|
||||||
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
description: "Allows you to stream in nitro quality, send fake emojis/stickers, use client themes and custom Discord notifications.",
|
||||||
dependencies: ["MessageEventsAPI"],
|
dependencies: ["MessageEventsAPI"],
|
||||||
|
|
||||||
settings,
|
settings,
|
||||||
|
@ -195,37 +204,43 @@ export default definePlugin({
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".PREMIUM_LOCKED;",
|
find: ".PREMIUM_LOCKED;",
|
||||||
|
group: true,
|
||||||
predicate: () => settings.store.enableEmojiBypass,
|
predicate: () => settings.store.enableEmojiBypass,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Create a variable for the intention of listing the emoji
|
// Create a variable for the intention of using the emoji
|
||||||
match: /(?<=,intention:(\i).+?;)/,
|
match: /(?<=\.USE_EXTERNAL_EMOJIS.+?;)(?<=intention:(\i).+?)/,
|
||||||
replace: (_, intention) => `let fakeNitroIntention=${intention};`
|
replace: (_, intention) => `const fakeNitroIntention=${intention};`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Send the intention of listing the emoji to the nitro permission check functions
|
// Disallow the emoji for external if the intention doesn't allow it
|
||||||
match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g,
|
match: /&&!\i&&!\i(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
||||||
replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0'
|
replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Disallow the emoji if the intention doesn't allow it
|
// Disallow the emoji for unavailable if the intention doesn't allow it
|
||||||
match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
match: /!\i\.available(?=\)return \i\.\i\.GUILD_SUBSCRIPTION_UNAVAILABLE;)/,
|
||||||
replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))`
|
replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Make the emoji always available if the intention allows it
|
// Disallow the emoji for premium locked if the intention doesn't allow it
|
||||||
match: /if\(!\i\.available/,
|
match: /!\i\.\i\.canUseEmojisEverywhere\(\i\)/,
|
||||||
replace: m => `${m}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
|
replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Allow animated emojis to be used if the intention allows it
|
||||||
|
match: /(?<=\|\|)\i\.\i\.canUseAnimatedEmojis\(\i\)/,
|
||||||
|
replace: m => `(${m}||${IS_BYPASSEABLE_INTENTION})`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// Allow emojis and animated emojis to be sent everywhere
|
// Allows the usage of subscription-locked emojis
|
||||||
{
|
{
|
||||||
find: "canUseAnimatedEmojis:function",
|
find: "isUnusableRoleSubscriptionEmoji:function",
|
||||||
predicate: () => settings.store.enableEmojiBypass,
|
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))(?=})/g,
|
match: /isUnusableRoleSubscriptionEmoji:function/,
|
||||||
replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
// Replace the original export with a func that always returns false and alias the original
|
||||||
|
replace: "isUnusableRoleSubscriptionEmoji:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Allow stickers to be sent everywhere
|
// Allow stickers to be sent everywhere
|
||||||
|
@ -239,10 +254,10 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// Make stickers always available
|
// Make stickers always available
|
||||||
{
|
{
|
||||||
find: "\"SENDABLE\"",
|
find: '"SENDABLE"',
|
||||||
predicate: () => settings.store.enableStickerBypass,
|
predicate: () => settings.store.enableStickerBypass,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(\w+)\.available\?/,
|
match: /\i\.available\?/,
|
||||||
replace: "true?"
|
replace: "true?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -397,6 +412,14 @@ export default definePlugin({
|
||||||
match: /(?<=type:"(?:SOUNDBOARD_SOUNDS_RECEIVED|GUILD_SOUNDBOARD_SOUND_CREATE|GUILD_SOUNDBOARD_SOUND_UPDATE|GUILD_SOUNDBOARD_SOUNDS_UPDATE)".+?available:)\i\.available/g,
|
match: /(?<=type:"(?:SOUNDBOARD_SOUNDS_RECEIVED|GUILD_SOUNDBOARD_SOUND_CREATE|GUILD_SOUNDBOARD_SOUND_UPDATE|GUILD_SOUNDBOARD_SOUNDS_UPDATE)".+?available:)\i\.available/g,
|
||||||
replace: "true"
|
replace: "true"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// Allow using custom notification sounds
|
||||||
|
{
|
||||||
|
find: "canUseCustomNotificationSounds:function",
|
||||||
|
replacement: {
|
||||||
|
match: /canUseCustomNotificationSounds:function\(\i\){/,
|
||||||
|
replace: "$&return true;"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -413,31 +436,35 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleProtoChange(proto: any, user: any) {
|
handleProtoChange(proto: any, user: any) {
|
||||||
if (proto == null || typeof proto === "string" || !UserSettingsProtoStore || !PreloadedUserSettingsActionCreators || !AppearanceSettingsActionCreators || !ClientThemeSettingsActionsCreators) return;
|
try {
|
||||||
|
if (proto == null || typeof proto === "string") return;
|
||||||
|
|
||||||
const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0;
|
const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0;
|
||||||
|
|
||||||
if (premiumType !== 2) {
|
if (premiumType !== 2) {
|
||||||
proto.appearance ??= AppearanceSettingsActionCreators.create();
|
proto.appearance ??= AppearanceSettingsActionCreators.create();
|
||||||
|
|
||||||
if (UserSettingsProtoStore.settings.appearance?.theme != null) {
|
if (UserSettingsProtoStore.settings.appearance?.theme != null) {
|
||||||
const appearanceSettingsDummy = AppearanceSettingsActionCreators.create({
|
const appearanceSettingsDummy = AppearanceSettingsActionCreators.create({
|
||||||
theme: UserSettingsProtoStore.settings.appearance.theme
|
theme: UserSettingsProtoStore.settings.appearance.theme
|
||||||
});
|
});
|
||||||
|
|
||||||
proto.appearance.theme = appearanceSettingsDummy.theme;
|
proto.appearance.theme = appearanceSettingsDummy.theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null) {
|
if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null) {
|
||||||
const clientThemeSettingsDummy = ClientThemeSettingsActionsCreators.create({
|
const clientThemeSettingsDummy = ClientThemeSettingsActionsCreators.create({
|
||||||
backgroundGradientPresetId: {
|
backgroundGradientPresetId: {
|
||||||
value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value
|
value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
proto.appearance.clientThemeSettings ??= clientThemeSettingsDummy;
|
proto.appearance.clientThemeSettings ??= clientThemeSettingsDummy;
|
||||||
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;
|
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
new Logger("FakeNitro").error(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -782,13 +809,16 @@ export default definePlugin({
|
||||||
gif.finish();
|
gif.finish();
|
||||||
|
|
||||||
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
|
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
|
||||||
UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
|
UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DraftType.ChannelMessage);
|
||||||
},
|
},
|
||||||
|
|
||||||
canUseEmote(e: CustomEmoji, channelId: string) {
|
canUseEmote(e: Emoji, channelId: string) {
|
||||||
if (e.require_colons === false) return true;
|
if (e.type === "UNICODE") return true;
|
||||||
if (e.available === false) return false;
|
if (e.available === false) return false;
|
||||||
|
|
||||||
|
const isUnusableRoleSubEmoji = RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
|
||||||
|
if (isUnusableRoleSubEmoji(e, this.guildId)) return false;
|
||||||
|
|
||||||
if (this.canUseEmotes)
|
if (this.canUseEmotes)
|
||||||
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
|
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
|
||||||
else
|
else
|
||||||
|
@ -900,7 +930,7 @@ export default definePlugin({
|
||||||
|
|
||||||
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
|
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
|
||||||
|
|
||||||
const url = new URL(emoji.url);
|
const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: s.emojiSize }));
|
||||||
url.searchParams.set("size", s.emojiSize.toString());
|
url.searchParams.set("size", s.emojiSize.toString());
|
||||||
url.searchParams.set("name", emoji.name);
|
url.searchParams.set("name", emoji.name);
|
||||||
|
|
||||||
|
@ -933,7 +963,7 @@ export default definePlugin({
|
||||||
|
|
||||||
hasBypass = true;
|
hasBypass = true;
|
||||||
|
|
||||||
const url = new URL(emoji.url);
|
const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: s.emojiSize }));
|
||||||
url.searchParams.set("size", s.emojiSize.toString());
|
url.searchParams.set("size", s.emojiSize.toString());
|
||||||
url.searchParams.set("name", emoji.name);
|
url.searchParams.set("name", emoji.name);
|
||||||
|
|
||||||
|
|
3
src/plugins/fakeProfileThemes/index.css
Normal file
3
src/plugins/fakeProfileThemes/index.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.vc-fpt-preview * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
|
@ -17,13 +17,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// This plugin is a port from Alyxia's Vendetta plugin
|
// This plugin is a port from Alyxia's Vendetta plugin
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { copyWithToast } from "@utils/misc";
|
import { classes, copyWithToast } from "@utils/misc";
|
||||||
|
import { useAwaiter } from "@utils/react";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { Button, Forms } from "@webpack/common";
|
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
|
||||||
|
import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
import virtualMerge from "virtual-merge";
|
import virtualMerge from "virtual-merge";
|
||||||
|
|
||||||
|
@ -81,6 +85,34 @@ const settings = definePluginSettings({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface ColorPickerProps {
|
||||||
|
color: number | null;
|
||||||
|
label: React.ReactElement;
|
||||||
|
showEyeDropper?: boolean;
|
||||||
|
suggestedColors?: string[];
|
||||||
|
onChange(value: number | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// I can't be bothered to figure out the semantics of this component. The
|
||||||
|
// functions surely get some event argument sent to them and they likely aren't
|
||||||
|
// all required. If anyone who wants to use this component stumbles across this
|
||||||
|
// code, you'll have to do the research yourself.
|
||||||
|
interface ProfileModalProps {
|
||||||
|
user: User;
|
||||||
|
pendingThemeColors: [number, number];
|
||||||
|
onAvatarChange: () => void;
|
||||||
|
onBannerChange: () => void;
|
||||||
|
canUsePremiumCustomization: boolean;
|
||||||
|
hideExampleButton: boolean;
|
||||||
|
hideFakeActivity: boolean;
|
||||||
|
isTryItOutFlow: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
|
||||||
|
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>('"ProfileCustomizationPreview"');
|
||||||
|
|
||||||
|
const requireColorPicker = extractAndLoadChunksLazy(["USER_SETTINGS_PROFILE_COLOR_DEFAULT_BUTTON.format"], /createPromise:\(\)=>\i\.\i\("(.+?)"\).then\(\i\.bind\(\i,"(.+?)"\)\)/);
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FakeProfileThemes",
|
name: "FakeProfileThemes",
|
||||||
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
|
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
|
||||||
|
@ -101,21 +133,98 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => {
|
||||||
<Forms.FormSection>
|
const existingColors = decode(
|
||||||
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
|
UserProfileStore.getUserProfile(UserStore.getCurrentUser().id).bio
|
||||||
<Forms.FormText>
|
) ?? [0, 0];
|
||||||
After enabling this plugin, you will see custom colors in the profiles of other people using compatible plugins. <br />
|
const [color1, setColor1] = useState(existingColors[0]);
|
||||||
To set your own colors:
|
const [color2, setColor2] = useState(existingColors[1]);
|
||||||
<ul>
|
|
||||||
<li>• go to your profile settings</li>
|
const [, , loadingColorPickerChunk] = useAwaiter(requireColorPicker);
|
||||||
<li>• choose your own colors in the Nitro preview</li>
|
|
||||||
<li>• click the "Copy 3y3" button</li>
|
return (
|
||||||
<li>• paste the invisible text anywhere in your bio</li>
|
<Forms.FormSection>
|
||||||
</ul><br />
|
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
|
||||||
<b>Please note:</b> if you are using a theme which hides nitro ads, you should disable it temporarily to set colors.
|
<Forms.FormText>
|
||||||
</Forms.FormText>
|
After enabling this plugin, you will see custom colors in
|
||||||
</Forms.FormSection>),
|
the profiles of other people using compatible plugins.{" "}
|
||||||
|
<br />
|
||||||
|
To set your own colors:
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
• use the color pickers below to choose your colors
|
||||||
|
</li>
|
||||||
|
<li>• click the "Copy 3y3" button</li>
|
||||||
|
<li>• paste the invisible text anywhere in your bio</li>
|
||||||
|
</ul><br />
|
||||||
|
<Forms.FormDivider
|
||||||
|
className={classes(Margins.top8, Margins.bottom8)}
|
||||||
|
/>
|
||||||
|
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
|
||||||
|
{!loadingColorPickerChunk && (
|
||||||
|
<Flex
|
||||||
|
direction={Flex.Direction.HORIZONTAL}
|
||||||
|
style={{ gap: "1rem" }}
|
||||||
|
>
|
||||||
|
<ColorPicker
|
||||||
|
color={color1}
|
||||||
|
label={
|
||||||
|
<Text
|
||||||
|
variant={"text-xs/normal"}
|
||||||
|
style={{ marginTop: "4px" }}
|
||||||
|
>
|
||||||
|
Primary
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
onChange={(color: number) => {
|
||||||
|
setColor1(color);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ColorPicker
|
||||||
|
color={color2}
|
||||||
|
label={
|
||||||
|
<Text
|
||||||
|
variant={"text-xs/normal"}
|
||||||
|
style={{ marginTop: "4px" }}
|
||||||
|
>
|
||||||
|
Accent
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
onChange={(color: number) => {
|
||||||
|
setColor2(color);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const colorString = encode(color1, color2);
|
||||||
|
copyWithToast(colorString);
|
||||||
|
}}
|
||||||
|
color={Button.Colors.PRIMARY}
|
||||||
|
size={Button.Sizes.XLARGE}
|
||||||
|
>
|
||||||
|
Copy 3y3
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<Forms.FormDivider
|
||||||
|
className={classes(Margins.top8, Margins.bottom8)}
|
||||||
|
/>
|
||||||
|
<Forms.FormTitle tag="h3">Preview</Forms.FormTitle>
|
||||||
|
<div className="vc-fpt-preview">
|
||||||
|
<ProfileModal
|
||||||
|
user={UserStore.getCurrentUser()}
|
||||||
|
pendingThemeColors={[color1, color2]}
|
||||||
|
onAvatarChange={() => { }}
|
||||||
|
onBannerChange={() => { }}
|
||||||
|
canUsePremiumCustomization={true}
|
||||||
|
hideExampleButton={true}
|
||||||
|
hideFakeActivity={true}
|
||||||
|
isTryItOutFlow={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Forms.FormText>
|
||||||
|
</Forms.FormSection>);
|
||||||
|
},
|
||||||
settings,
|
settings,
|
||||||
colorDecodeHook(user: UserProfile) {
|
colorDecodeHook(user: UserProfile) {
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption,
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { RestAPI, UserStore } from "@webpack/common";
|
import { Constants, RestAPI, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
const FriendInvites = findByPropsLazy("createFriendInvite");
|
const FriendInvites = findByPropsLazy("createFriendInvite");
|
||||||
const { uuid4 } = findByPropsLazy("uuid4");
|
const { uuid4 } = findByPropsLazy("uuid4");
|
||||||
|
@ -58,7 +58,7 @@ export default definePlugin({
|
||||||
if (uses === 1) {
|
if (uses === 1) {
|
||||||
const random = uuid4();
|
const random = uuid4();
|
||||||
const { body: { invite_suggestions } } = await RestAPI.post({
|
const { body: { invite_suggestions } } = await RestAPI.post({
|
||||||
url: "/friend-finder/find-friends",
|
url: Constants.Endpoints.FRIEND_FINDER,
|
||||||
body: {
|
body: {
|
||||||
modified_contacts: {
|
modified_contacts: {
|
||||||
[random]: [1, "", ""]
|
[random]: [1, "", ""]
|
||||||
|
|
|
@ -7,11 +7,12 @@
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { getCurrentChannel } from "@utils/discord";
|
import { getCurrentChannel } from "@utils/discord";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { React, RelationshipStore } from "@webpack/common";
|
import { Heading, React, RelationshipStore, Text } from "@webpack/common";
|
||||||
|
|
||||||
const { Heading, Text } = findByPropsLazy("Heading", "Text");
|
|
||||||
const container = findByPropsLazy("memberSinceWrapper");
|
const container = findByPropsLazy("memberSinceWrapper");
|
||||||
const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate");
|
const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate");
|
||||||
const clydeMoreInfo = findByPropsLazy("clydeMoreInfo");
|
const clydeMoreInfo = findByPropsLazy("clydeMoreInfo");
|
||||||
|
@ -23,6 +24,7 @@ export default definePlugin({
|
||||||
description: "Shows when you became friends with someone in the user popout",
|
description: "Shows when you became friends with someone in the user popout",
|
||||||
authors: [Devs.Elvyra],
|
authors: [Devs.Elvyra],
|
||||||
patches: [
|
patches: [
|
||||||
|
// User popup
|
||||||
{
|
{
|
||||||
find: ".AnalyticsSections.USER_PROFILE}",
|
find: ".AnalyticsSections.USER_PROFILE}",
|
||||||
replacement: {
|
replacement: {
|
||||||
|
@ -30,16 +32,34 @@ export default definePlugin({
|
||||||
replace: "$&,$self.friendsSince({ userId: $1 })"
|
replace: "$&,$self.friendsSince({ userId: $1 })"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// User DMs "User Profile" popup in the right
|
||||||
{
|
{
|
||||||
find: ".UserPopoutUpsellSource.PROFILE_PANEL,",
|
find: ".UserPopoutUpsellSource.PROFILE_PANEL,",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\i.default,\{userId:(\i)}\)/,
|
match: /\i.default,\{userId:(\i)}\)/,
|
||||||
replace: "$&,$self.friendsSince({ userId: $1 })"
|
replace: "$&,$self.friendsSince({ userId: $1 })"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// User Profile Modal
|
||||||
|
{
|
||||||
|
find: ".userInfoSectionHeader,",
|
||||||
|
replacement: {
|
||||||
|
match: /(\.Messages\.USER_PROFILE_MEMBER_SINCE.+?userId:(.+?),textClassName:)(\i\.userInfoText)}\)/,
|
||||||
|
replace: (_, rest, userId, textClassName) => `${rest}!$self.getFriendSince(${userId}) ? ${textClassName} : void 0 }), $self.friendsSince({ userId: ${userId}, textClassName: ${textClassName} })`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
friendsSince: ErrorBoundary.wrap(({ userId }: { userId: string; }) => {
|
getFriendSince(userId: string) {
|
||||||
|
try {
|
||||||
|
return RelationshipStore.getSince(userId);
|
||||||
|
} catch (err) {
|
||||||
|
new Logger("FriendsSince").error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
friendsSince: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => {
|
||||||
const friendsSince = RelationshipStore.getSince(userId);
|
const friendsSince = RelationshipStore.getSince(userId);
|
||||||
if (!friendsSince) return null;
|
if (!friendsSince) return null;
|
||||||
|
|
||||||
|
@ -62,7 +82,7 @@ export default definePlugin({
|
||||||
<path d="M3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z" />
|
<path d="M3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
<Text variant="text-sm/normal" className={clydeMoreInfo.body}>
|
<Text variant="text-sm/normal" className={classes(clydeMoreInfo.body, textClassName)}>
|
||||||
{getCreatedAtDate(friendsSince, locale.getLocale())}
|
{getCreatedAtDate(friendsSince, locale.getLocale())}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,4 +90,3 @@ export default definePlugin({
|
||||||
);
|
);
|
||||||
}, { noop: true })
|
}, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
3
src/plugins/imageLink/README.md
Normal file
3
src/plugins/imageLink/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# ImageLink
|
||||||
|
|
||||||
|
If a message consists of only a link to an image, Discord hides the link and shows only the image embed. This plugin makes the link show regardless.
|
24
src/plugins/imageLink/index.ts
Normal file
24
src/plugins/imageLink/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ImageLink",
|
||||||
|
description: "Never hide image links in messages, even if it's the only content",
|
||||||
|
authors: [Devs.Kyuuhachi, Devs.Sqaaakoi],
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "unknownUserMentionPlaceholder:",
|
||||||
|
replacement: {
|
||||||
|
match: /\(0,\i\.isEmbedInline\)\(\i\)/,
|
||||||
|
replace: "false",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
|
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
|
||||||
|
|
||||||
import { ELEMENT_ID } from "../constants";
|
import { ELEMENT_ID } from "../constants";
|
||||||
|
@ -36,7 +37,7 @@ export interface MagnifierProps {
|
||||||
|
|
||||||
const cl = classNameFactory("vc-imgzoom-");
|
const cl = classNameFactory("vc-imgzoom-");
|
||||||
|
|
||||||
export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSize, zoom: initalZoom }) => {
|
export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: initialSize, zoom: initalZoom }) => {
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });
|
const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });
|
||||||
|
@ -199,4 +200,4 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}, { noop: true });
|
||||||
|
|
|
@ -19,11 +19,12 @@
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByProps, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
import { ChannelStore, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common";
|
import { ChannelStore, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common";
|
||||||
import { Settings } from "Vencord";
|
import { Settings } from "Vencord";
|
||||||
|
|
||||||
const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore");
|
const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore");
|
||||||
|
const { FriendsSections } = findByPropsLazy("FriendsSections");
|
||||||
|
|
||||||
interface UserAffinity {
|
interface UserAffinity {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
|
@ -81,8 +82,8 @@ export default definePlugin({
|
||||||
find: "getRelationshipCounts(){",
|
find: "getRelationshipCounts(){",
|
||||||
replacement: {
|
replacement: {
|
||||||
predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity,
|
predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity,
|
||||||
match: /\.sortBy\(\i=>\i\.comparator\)/,
|
match: /\}\)\.sortBy\((.+?)\)\.value\(\)/,
|
||||||
replace: "$&.sortBy((row) => $self.sortList(row))"
|
replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -120,10 +121,10 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
sortList(row: any) {
|
wrapSort(comparator: Function, row: any) {
|
||||||
return row.type === 5
|
return row.type === 5
|
||||||
? -UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0
|
? -UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0
|
||||||
: row.comparator;
|
: comparator(row);
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchImplicitRelationships() {
|
async fetchImplicitRelationships() {
|
||||||
|
@ -151,20 +152,25 @@ export default definePlugin({
|
||||||
// OP 8 Request Guild Members allows 100 user IDs at a time
|
// OP 8 Request Guild Members allows 100 user IDs at a time
|
||||||
const ignore = new Set(toRequest);
|
const ignore = new Set(toRequest);
|
||||||
const relationships = RelationshipStore.getRelationships();
|
const relationships = RelationshipStore.getRelationships();
|
||||||
const callback = ({ nonce, members }) => {
|
const callback = ({ chunks }) => {
|
||||||
if (nonce !== sentNonce) return;
|
for (const chunk of chunks) {
|
||||||
members.forEach(member => {
|
const { nonce, members } = chunk;
|
||||||
ignore.delete(member.user.id);
|
if (nonce !== sentNonce) return;
|
||||||
});
|
members.forEach(member => {
|
||||||
|
ignore.delete(member.user.id);
|
||||||
|
});
|
||||||
|
|
||||||
nonFriendAffinities.map(id => UserStore.getUser(id)).filter(user => user && !ignore.has(user.id)).forEach(user => relationships[user.id] = 5);
|
nonFriendAffinities.map(id => UserStore.getUser(id)).filter(user => user && !ignore.has(user.id)).forEach(user => relationships[user.id] = 5);
|
||||||
RelationshipStore.emitChange();
|
RelationshipStore.emitChange();
|
||||||
if (--count === 0) {
|
if (--count === 0) {
|
||||||
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK", callback);
|
// @ts-ignore
|
||||||
|
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK", callback);
|
// @ts-ignore
|
||||||
|
FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
|
||||||
for (let i = 0; i < toRequest.length; i += 100) {
|
for (let i = 0; i < toRequest.length; i += 100) {
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
type: "GUILD_MEMBERS_REQUEST",
|
type: "GUILD_MEMBERS_REQUEST",
|
||||||
|
@ -176,7 +182,6 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
const { FriendsSections } = findByProps("FriendsSections");
|
|
||||||
FriendsSections.IMPLICIT = "IMPLICIT";
|
FriendsSections.IMPLICIT = "IMPLICIT";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { registerCommand, unregisterCommand } from "@api/Commands";
|
||||||
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
|
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
|
import { canonicalizeFind } from "@utils/patches";
|
||||||
import { Patch, Plugin, StartAt } from "@utils/types";
|
import { Patch, Plugin, StartAt } from "@utils/types";
|
||||||
import { FluxDispatcher } from "@webpack/common";
|
import { FluxDispatcher } from "@webpack/common";
|
||||||
import { FluxEvents } from "@webpack/types";
|
import { FluxEvents } from "@webpack/types";
|
||||||
|
@ -34,6 +35,10 @@ export const PMLogger = logger;
|
||||||
export const plugins = Plugins;
|
export const plugins = Plugins;
|
||||||
export const patches = [] as Patch[];
|
export const patches = [] as Patch[];
|
||||||
|
|
||||||
|
/** Whether we have subscribed to flux events of all the enabled plugins when FluxDispatcher was ready */
|
||||||
|
let enabledPluginsSubscribedFlux = false;
|
||||||
|
const subscribedFluxEventsPlugins = new Set<string>();
|
||||||
|
|
||||||
const settings = Settings.plugins;
|
const settings = Settings.plugins;
|
||||||
|
|
||||||
export function isPluginEnabled(p: string) {
|
export function isPluginEnabled(p: string) {
|
||||||
|
@ -79,8 +84,12 @@ for (const p of pluginsValues) {
|
||||||
if (p.patches && isPluginEnabled(p.name)) {
|
if (p.patches && isPluginEnabled(p.name)) {
|
||||||
for (const patch of p.patches) {
|
for (const patch of p.patches) {
|
||||||
patch.plugin = p.name;
|
patch.plugin = p.name;
|
||||||
if (!Array.isArray(patch.replacement))
|
|
||||||
|
canonicalizeFind(patch);
|
||||||
|
if (!Array.isArray(patch.replacement)) {
|
||||||
patch.replacement = [patch.replacement];
|
patch.replacement = [patch.replacement];
|
||||||
|
}
|
||||||
|
|
||||||
patches.push(patch);
|
patches.push(patch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +128,37 @@ export function startDependenciesRecursive(p: Plugin) {
|
||||||
return { restartNeeded, failures };
|
return { restartNeeded, failures };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function subscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) {
|
||||||
|
if (p.flux && !subscribedFluxEventsPlugins.has(p.name)) {
|
||||||
|
subscribedFluxEventsPlugins.add(p.name);
|
||||||
|
|
||||||
|
logger.debug("Subscribing to flux events of plugin", p.name);
|
||||||
|
for (const [event, handler] of Object.entries(p.flux)) {
|
||||||
|
fluxDispatcher.subscribe(event as FluxEvents, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unsubscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) {
|
||||||
|
if (p.flux) {
|
||||||
|
subscribedFluxEventsPlugins.delete(p.name);
|
||||||
|
|
||||||
|
logger.debug("Unsubscribing from flux events of plugin", p.name);
|
||||||
|
for (const [event, handler] of Object.entries(p.flux)) {
|
||||||
|
fluxDispatcher.unsubscribe(event as FluxEvents, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeAllPluginsFluxEvents(fluxDispatcher: typeof FluxDispatcher) {
|
||||||
|
enabledPluginsSubscribedFlux = true;
|
||||||
|
|
||||||
|
for (const name in Plugins) {
|
||||||
|
if (!isPluginEnabled(name)) continue;
|
||||||
|
subscribePluginFluxEvents(Plugins[name], fluxDispatcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
|
export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
|
||||||
const { name, commands, flux, contextMenus } = p;
|
const { name, commands, flux, contextMenus } = p;
|
||||||
|
|
||||||
|
@ -130,15 +170,16 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
p.start();
|
p.start();
|
||||||
p.started = true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Failed to start ${name}\n`, e);
|
logger.error(`Failed to start ${name}\n`, e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.started = true;
|
||||||
|
|
||||||
if (commands?.length) {
|
if (commands?.length) {
|
||||||
logger.info("Registering commands of plugin", name);
|
logger.debug("Registering commands of plugin", name);
|
||||||
for (const cmd of commands) {
|
for (const cmd of commands) {
|
||||||
try {
|
try {
|
||||||
registerCommand(cmd, name);
|
registerCommand(cmd, name);
|
||||||
|
@ -149,13 +190,13 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flux) {
|
if (enabledPluginsSubscribedFlux) {
|
||||||
for (const event in flux) {
|
subscribePluginFluxEvents(p, FluxDispatcher);
|
||||||
FluxDispatcher.subscribe(event as FluxEvents, flux[event]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (contextMenus) {
|
if (contextMenus) {
|
||||||
|
logger.debug("Adding context menus patches of plugin", name);
|
||||||
for (const navId in contextMenus) {
|
for (const navId in contextMenus) {
|
||||||
addContextMenuPatch(navId, contextMenus[navId]);
|
addContextMenuPatch(navId, contextMenus[navId]);
|
||||||
}
|
}
|
||||||
|
@ -166,6 +207,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
||||||
|
|
||||||
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
|
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
|
||||||
const { name, commands, flux, contextMenus } = p;
|
const { name, commands, flux, contextMenus } = p;
|
||||||
|
|
||||||
if (p.stop) {
|
if (p.stop) {
|
||||||
logger.info("Stopping plugin", name);
|
logger.info("Stopping plugin", name);
|
||||||
if (!p.started) {
|
if (!p.started) {
|
||||||
|
@ -174,15 +216,16 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
p.stop();
|
p.stop();
|
||||||
p.started = false;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Failed to stop ${name}\n`, e);
|
logger.error(`Failed to stop ${name}\n`, e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.started = false;
|
||||||
|
|
||||||
if (commands?.length) {
|
if (commands?.length) {
|
||||||
logger.info("Unregistering commands of plugin", name);
|
logger.debug("Unregistering commands of plugin", name);
|
||||||
for (const cmd of commands) {
|
for (const cmd of commands) {
|
||||||
try {
|
try {
|
||||||
unregisterCommand(cmd.name);
|
unregisterCommand(cmd.name);
|
||||||
|
@ -193,13 +236,10 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flux) {
|
unsubscribePluginFluxEvents(p, FluxDispatcher);
|
||||||
for (const event in flux) {
|
|
||||||
FluxDispatcher.unsubscribe(event as FluxEvents, flux[event]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contextMenus) {
|
if (contextMenus) {
|
||||||
|
logger.debug("Removing context menus patches of plugin", name);
|
||||||
for (const navId in contextMenus) {
|
for (const navId in contextMenus) {
|
||||||
removeContextMenuPatch(navId, contextMenus[navId]);
|
removeContextMenuPatch(navId, contextMenus[navId]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { getStegCloak } from "@utils/dependencies";
|
import { getStegCloak } from "@utils/dependencies";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common";
|
import { ChannelStore, Constants, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
import { buildDecModal } from "./components/DecryptionModal";
|
import { buildDecModal } from "./components/DecryptionModal";
|
||||||
|
@ -153,7 +153,7 @@ export default definePlugin({
|
||||||
// Gets the Embed of a Link
|
// Gets the Embed of a Link
|
||||||
async getEmbed(url: URL): Promise<Object | {}> {
|
async getEmbed(url: URL): Promise<Object | {}> {
|
||||||
const { body } = await RestAPI.post({
|
const { body } = await RestAPI.post({
|
||||||
url: "/unfurler/embed-urls",
|
url: Constants.Endpoints.UNFURL_EMBED_URLS,
|
||||||
body: {
|
body: {
|
||||||
urls: [url]
|
urls: [url]
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,8 @@ const enum NameFormat {
|
||||||
ArtistFirst = "artist-first",
|
ArtistFirst = "artist-first",
|
||||||
SongFirst = "song-first",
|
SongFirst = "song-first",
|
||||||
ArtistOnly = "artist",
|
ArtistOnly = "artist",
|
||||||
SongOnly = "song"
|
SongOnly = "song",
|
||||||
|
AlbumName = "album"
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationId = "1108588077900898414";
|
const applicationId = "1108588077900898414";
|
||||||
|
@ -113,6 +114,11 @@ const settings = definePluginSettings({
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
shareSong: {
|
||||||
|
description: "show link to song on last.fm",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
hideWithSpotify: {
|
hideWithSpotify: {
|
||||||
description: "hide last.fm presence if spotify is running",
|
description: "hide last.fm presence if spotify is running",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
|
@ -147,6 +153,10 @@ const settings = definePluginSettings({
|
||||||
{
|
{
|
||||||
label: "Use song name only",
|
label: "Use song name only",
|
||||||
value: NameFormat.SongOnly
|
value: NameFormat.SongOnly
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use album name (falls back to custom status text if song has no album)",
|
||||||
|
value: NameFormat.AlbumName
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -290,12 +300,7 @@ export default definePlugin({
|
||||||
large_text: trackData.album || undefined,
|
large_text: trackData.album || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttons: ActivityButton[] = [
|
const buttons: ActivityButton[] = [];
|
||||||
{
|
|
||||||
label: "View Song",
|
|
||||||
url: trackData.url,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (settings.store.shareUsername)
|
if (settings.store.shareUsername)
|
||||||
buttons.push({
|
buttons.push({
|
||||||
|
@ -303,6 +308,12 @@ export default definePlugin({
|
||||||
url: `https://www.last.fm/user/${settings.store.username}`,
|
url: `https://www.last.fm/user/${settings.store.username}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (settings.store.shareSong)
|
||||||
|
buttons.push({
|
||||||
|
label: "View Song",
|
||||||
|
url: trackData.url,
|
||||||
|
});
|
||||||
|
|
||||||
const statusName = (() => {
|
const statusName = (() => {
|
||||||
switch (settings.store.nameFormat) {
|
switch (settings.store.nameFormat) {
|
||||||
case NameFormat.ArtistFirst:
|
case NameFormat.ArtistFirst:
|
||||||
|
@ -313,6 +324,8 @@ export default definePlugin({
|
||||||
return trackData.artist;
|
return trackData.artist;
|
||||||
case NameFormat.SongOnly:
|
case NameFormat.SongOnly:
|
||||||
return trackData.name;
|
return trackData.name;
|
||||||
|
case NameFormat.AlbumName:
|
||||||
|
return trackData.album || settings.store.statusName;
|
||||||
default:
|
default:
|
||||||
return settings.store.statusName;
|
return settings.store.statusName;
|
||||||
}
|
}
|
||||||
|
@ -326,7 +339,7 @@ export default definePlugin({
|
||||||
state: trackData.artist,
|
state: trackData.artist,
|
||||||
assets,
|
assets,
|
||||||
|
|
||||||
buttons: buttons.map(v => v.label),
|
buttons: buttons.length ? buttons.map(v => v.label) : undefined,
|
||||||
metadata: {
|
metadata: {
|
||||||
button_urls: buttons.map(v => v.url),
|
button_urls: buttons.map(v => v.url),
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,17 +17,19 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addClickListener, removeClickListener } from "@api/MessageEvents";
|
import { addClickListener, removeClickListener } from "@api/MessageEvents";
|
||||||
import { definePluginSettings, Settings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore } from "@webpack/common";
|
import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
|
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
||||||
|
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
||||||
|
|
||||||
let isDeletePressed = false;
|
let isDeletePressed = false;
|
||||||
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
||||||
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
|
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
|
||||||
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
enableDeleteOnClick: {
|
enableDeleteOnClick: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
|
@ -60,9 +62,6 @@ export default definePlugin({
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
|
||||||
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
|
||||||
|
|
||||||
document.addEventListener("keydown", keydown);
|
document.addEventListener("keydown", keydown);
|
||||||
document.addEventListener("keyup", keyup);
|
document.addEventListener("keyup", keyup);
|
||||||
|
|
||||||
|
@ -85,11 +84,17 @@ export default definePlugin({
|
||||||
const EPHEMERAL = 64;
|
const EPHEMERAL = 64;
|
||||||
if (msg.hasFlag(EPHEMERAL)) return;
|
if (msg.hasFlag(EPHEMERAL)) return;
|
||||||
|
|
||||||
|
const isShiftPress = event.shiftKey && !settings.store.requireModifier;
|
||||||
|
const NoReplyMention = Vencord.Plugins.plugins.NoReplyMention as any as typeof import("../noReplyMention").default;
|
||||||
|
const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention")
|
||||||
|
? NoReplyMention.shouldMention(msg, isShiftPress)
|
||||||
|
: !isShiftPress;
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
type: "CREATE_PENDING_REPLY",
|
type: "CREATE_PENDING_REPLY",
|
||||||
channel,
|
channel,
|
||||||
message: msg,
|
message: msg,
|
||||||
shouldMention: !Settings.plugins.NoReplyMention.enabled,
|
shouldMention,
|
||||||
showMentionToggle: channel.guild_id !== null
|
showMentionToggle: channel.guild_id !== null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
31
src/plugins/messageLatency/README.md
Normal file
31
src/plugins/messageLatency/README.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# MessageLatency
|
||||||
|
|
||||||
|
Displays an indicator for messages that took ≥n seconds to send.
|
||||||
|
|
||||||
|
> **NOTE**
|
||||||
|
>
|
||||||
|
> - This plugin only applies to messages received after opening the channel
|
||||||
|
> - False positives can exist if the user's system clock has drifted.
|
||||||
|
> - Grouped messages only display latency of the first message
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
### Chat View
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Clock -ve Drift
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Clock +ve Drift
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Connection Delay
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Icons
|
||||||
|
|
||||||
|

|
204
src/plugins/messageLatency/index.tsx
Normal file
204
src/plugins/messageLatency/index.tsx
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { isNonNullish } from "@utils/guards";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { findExportedComponentLazy } from "@webpack";
|
||||||
|
import { SnowflakeUtils, Tooltip } from "@webpack/common";
|
||||||
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
type FillValue = ("status-danger" | "status-warning" | "status-positive" | "text-muted");
|
||||||
|
type Fill = [FillValue, FillValue, FillValue];
|
||||||
|
type DiffKey = keyof Diff;
|
||||||
|
|
||||||
|
interface Diff {
|
||||||
|
days: number,
|
||||||
|
hours: number,
|
||||||
|
minutes: number,
|
||||||
|
seconds: number;
|
||||||
|
milliseconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DISCORD_KT_DELAY = 1471228928;
|
||||||
|
const HiddenVisually = findExportedComponentLazy("HiddenVisually");
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "MessageLatency",
|
||||||
|
description: "Displays an indicator for messages that took ≥n seconds to send",
|
||||||
|
authors: [Devs.arHSM],
|
||||||
|
|
||||||
|
settings: definePluginSettings({
|
||||||
|
latency: {
|
||||||
|
type: OptionType.NUMBER,
|
||||||
|
description: "Threshold in seconds for latency indicator",
|
||||||
|
default: 2
|
||||||
|
},
|
||||||
|
detectDiscordKotlin: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Detect old Discord Android clients",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
showMillis: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Show milliseconds",
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "showCommunicationDisabledStyles",
|
||||||
|
replacement: {
|
||||||
|
match: /(message:(\i),avatar:\i,username:\(0,\i.jsxs\)\(\i.Fragment,\{children:\[)(\i&&)/,
|
||||||
|
replace: "$1$self.Tooltip()({ message: $2 }),$3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
stringDelta(delta: number, showMillis: boolean) {
|
||||||
|
const diff: Diff = {
|
||||||
|
days: Math.round(delta / (60 * 60 * 24 * 1000)),
|
||||||
|
hours: Math.round((delta / (60 * 60 * 1000)) % 24),
|
||||||
|
minutes: Math.round((delta / (60 * 1000)) % 60),
|
||||||
|
seconds: Math.round(delta / 1000 % 60),
|
||||||
|
milliseconds: Math.round(delta % 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;
|
||||||
|
const keys = Object.keys(diff) as DiffKey[];
|
||||||
|
|
||||||
|
const ts = keys.reduce((prev, k) => {
|
||||||
|
const s = str(k);
|
||||||
|
|
||||||
|
return prev + (
|
||||||
|
isNonNullish(s)
|
||||||
|
? (prev !== ""
|
||||||
|
? (showMillis ? k === "milliseconds" : k === "seconds")
|
||||||
|
? " and "
|
||||||
|
: " "
|
||||||
|
: "") + s
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
}, "");
|
||||||
|
|
||||||
|
return ts || "0 seconds";
|
||||||
|
},
|
||||||
|
|
||||||
|
latencyTooltipData(message: Message) {
|
||||||
|
const { latency, detectDiscordKotlin, showMillis } = this.settings.store;
|
||||||
|
const { id, nonce } = message;
|
||||||
|
|
||||||
|
// Message wasn't received through gateway
|
||||||
|
if (!isNonNullish(nonce)) return null;
|
||||||
|
|
||||||
|
let isDiscordKotlin = false;
|
||||||
|
let delta = SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce); // milliseconds
|
||||||
|
if (!showMillis) {
|
||||||
|
delta = Math.round(delta / 1000) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old Discord Android clients have a delay of around 17 days
|
||||||
|
// This is a workaround for that
|
||||||
|
if (-delta >= DISCORD_KT_DELAY - 86400000) { // One day of padding for good measure
|
||||||
|
isDiscordKotlin = detectDiscordKotlin;
|
||||||
|
delta += DISCORD_KT_DELAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thanks dziurwa (I hate you)
|
||||||
|
// This is when the user's clock is ahead
|
||||||
|
// Can't do anything if the clock is behind
|
||||||
|
const abs = Math.abs(delta);
|
||||||
|
const ahead = abs !== delta;
|
||||||
|
const latencyMillis = latency * 1000;
|
||||||
|
|
||||||
|
const stringDelta = abs >= latencyMillis ? this.stringDelta(abs, showMillis) : null;
|
||||||
|
|
||||||
|
// Also thanks dziurwa
|
||||||
|
// 2 minutes
|
||||||
|
const TROLL_LIMIT = 2 * 60 * 1000;
|
||||||
|
|
||||||
|
const fill: Fill = isDiscordKotlin
|
||||||
|
? ["status-positive", "status-positive", "text-muted"]
|
||||||
|
: delta >= TROLL_LIMIT || ahead
|
||||||
|
? ["text-muted", "text-muted", "text-muted"]
|
||||||
|
: delta >= (latencyMillis * 2)
|
||||||
|
? ["status-danger", "text-muted", "text-muted"]
|
||||||
|
: ["status-warning", "status-warning", "text-muted"];
|
||||||
|
|
||||||
|
return (abs >= latencyMillis || isDiscordKotlin) ? { delta: stringDelta, ahead, fill, isDiscordKotlin } : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
Tooltip() {
|
||||||
|
return ErrorBoundary.wrap(({ message }: { message: Message; }) => {
|
||||||
|
const d = this.latencyTooltipData(message);
|
||||||
|
|
||||||
|
if (!isNonNullish(d)) return null;
|
||||||
|
|
||||||
|
let text: string;
|
||||||
|
if (!d.delta) {
|
||||||
|
text = "User is suspected to be on an old Discord Android client";
|
||||||
|
} else {
|
||||||
|
text = (d.ahead ? `This user's clock is ${d.delta} ahead.` : `This message was sent with a delay of ${d.delta}.`) + (d.isDiscordKotlin ? " User is suspected to be on an old Discord Android client." : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Tooltip
|
||||||
|
text={text}
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
props => <>
|
||||||
|
{<this.Icon delta={d.delta} fill={d.fill} props={props} />}
|
||||||
|
{/* Time Out indicator uses this, I think this is for a11y */}
|
||||||
|
<HiddenVisually>Delayed Message</HiddenVisually>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</Tooltip>;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
Icon({ delta, fill, props }: {
|
||||||
|
delta: string | null;
|
||||||
|
fill: Fill,
|
||||||
|
props: {
|
||||||
|
onClick(): void;
|
||||||
|
onMouseEnter(): void;
|
||||||
|
onMouseLeave(): void;
|
||||||
|
onContextMenu(): void;
|
||||||
|
onFocus(): void;
|
||||||
|
onBlur(): void;
|
||||||
|
"aria-label"?: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return <svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
role="img"
|
||||||
|
fill="none"
|
||||||
|
style={{ marginRight: "8px", verticalAlign: -1 }}
|
||||||
|
aria-label={delta ?? "Old Discord Android client"}
|
||||||
|
aria-hidden="false"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill={`var(--${fill[0]})`}
|
||||||
|
d="M4.8001 12C4.8001 11.5576 4.51344 11.2 4.16023 11.2H2.23997C1.88676 11.2 1.6001 11.5576 1.6001 12V13.6C1.6001 14.0424 1.88676 14.4 2.23997 14.4H4.15959C4.5128 14.4 4.79946 14.0424 4.79946 13.6L4.8001 12Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill={`var(--${fill[1]})`}
|
||||||
|
d="M9.6001 7.12724C9.6001 6.72504 9.31337 6.39998 8.9601 6.39998H7.0401C6.68684 6.39998 6.40011 6.72504 6.40011 7.12724V13.6727C6.40011 14.0749 6.68684 14.4 7.0401 14.4H8.9601C9.31337 14.4 9.6001 14.0749 9.6001 13.6727V7.12724Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill={`var(--${fill[2]})`}
|
||||||
|
d="M14.4001 2.31109C14.4001 1.91784 14.1134 1.59998 13.7601 1.59998H11.8401C11.4868 1.59998 11.2001 1.91784 11.2001 2.31109V13.6888C11.2001 14.0821 11.4868 14.4 11.8401 14.4H13.7601C14.1134 14.4 14.4001 14.0821 14.4001 13.6888V2.31109Z"
|
||||||
|
/>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
});
|
|
@ -27,6 +27,7 @@ import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ChannelStore,
|
ChannelStore,
|
||||||
|
Constants,
|
||||||
FluxDispatcher,
|
FluxDispatcher,
|
||||||
GuildStore,
|
GuildStore,
|
||||||
IconUtils,
|
IconUtils,
|
||||||
|
@ -132,7 +133,7 @@ async function fetchMessage(channelID: string, messageID: string) {
|
||||||
messageCache.set(messageID, { fetched: false });
|
messageCache.set(messageID, { fetched: false });
|
||||||
|
|
||||||
const res = await RestAPI.get({
|
const res = await RestAPI.get({
|
||||||
url: `/channels/${channelID}/messages`,
|
url: Constants.Endpoints.MESSAGES(channelID),
|
||||||
query: {
|
query: {
|
||||||
limit: 1,
|
limit: 1,
|
||||||
around: messageID
|
around: messageID
|
||||||
|
@ -226,10 +227,8 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
|
||||||
|
|
||||||
const accessories = [] as (JSX.Element | null)[];
|
const accessories = [] as (JSX.Element | null)[];
|
||||||
|
|
||||||
let match = null as RegExpMatchArray | null;
|
for (const [_, channelID, messageID] of message.content!.matchAll(messageLinkRegex)) {
|
||||||
while ((match = messageLinkRegex.exec(message.content!)) !== null) {
|
if (embeddedBy.includes(messageID) || embeddedBy.length > 2) {
|
||||||
const [_, channelID, messageID] = match;
|
|
||||||
if (embeddedBy.includes(messageID)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
.messagelogger-deleted {
|
.messagelogger-deleted {
|
||||||
background-color: rgba(240 71 71 / 15%) !important;
|
background-color: hsla(var(--red-430-hsl, 0 85% 61%) / 15%) !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
/* Message content highlighting */
|
/* Message content highlighting */
|
||||||
.messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) {
|
.messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) {
|
||||||
color: #f04747 !important;
|
color: var(--status-danger, #f04747) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bot "thinking" text highlighting */
|
/* Bot "thinking" text highlighting */
|
||||||
.messagelogger-deleted [class*="colorStandard"] {
|
.messagelogger-deleted [class*="colorStandard"] {
|
||||||
color: #f04747 !important;
|
color: var(--status-danger, #f04747) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Embed highlighting */
|
/* Embed highlighting */
|
||||||
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
|
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
|
||||||
color: #f04747 !important;
|
color: var(--status-danger, #f04747) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messagelogger-deleted a {
|
.messagelogger-deleted a {
|
||||||
color: #be3535 !important;
|
color: var(--red-460, #be3535) !important;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
|
@ -217,7 +217,9 @@ export default definePlugin({
|
||||||
ignoreChannels.includes(message.channel_id) ||
|
ignoreChannels.includes(message.channel_id) ||
|
||||||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
|
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
|
||||||
(isEdit ? !logEdits : !logDeletes) ||
|
(isEdit ? !logEdits : !logDeletes) ||
|
||||||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
|
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id) ||
|
||||||
|
// Ignore Venbot in the support channel
|
||||||
|
(message.channel_id === "1026515880080842772" && message.author?.id === "1017176847865352332");
|
||||||
},
|
},
|
||||||
|
|
||||||
// Based on canary 63b8f1b4f2025213c5cf62f0966625bee3d53136
|
// Based on canary 63b8f1b4f2025213c5cf62f0966625bee3d53136
|
||||||
|
@ -293,12 +295,9 @@ export default definePlugin({
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
// Pass through editHistory & deleted & original attachments to the "edited message" transformer
|
// Pass through editHistory & deleted & original attachments to the "edited message" transformer
|
||||||
match: /interactionData:(\i)\.interactionData/,
|
match: /(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/,
|
||||||
replace:
|
replace:
|
||||||
"interactionData:$1.interactionData," +
|
"Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, attachments:$1.attachments })"
|
||||||
"deleted:$1.deleted," +
|
|
||||||
"editHistory:$1.editHistory," +
|
|
||||||
"attachments:$1.attachments"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
.emoji,
|
.emoji,
|
||||||
[data-type="sticker"],
|
[data-type="sticker"],
|
||||||
iframe,
|
iframe,
|
||||||
.messagelogger-deleted-attachment,
|
.messagelogger-deleted-attachment:not([class*="hiddenAttachment_"]),
|
||||||
[class|="inlineMediaEmbed"]
|
[class|="inlineMediaEmbed"]
|
||||||
) {
|
) {
|
||||||
filter: grayscale(1) !important;
|
filter: grayscale(1) !important;
|
||||||
|
|
|
@ -50,6 +50,7 @@ interface TagSettings {
|
||||||
MODERATOR_STAFF: TagSetting,
|
MODERATOR_STAFF: TagSetting,
|
||||||
MODERATOR: TagSetting,
|
MODERATOR: TagSetting,
|
||||||
VOICE_MODERATOR: TagSetting,
|
VOICE_MODERATOR: TagSetting,
|
||||||
|
TRIAL_MODERATOR: TagSetting,
|
||||||
[k: string]: TagSetting;
|
[k: string]: TagSetting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +94,11 @@ const tags: Tag[] = [
|
||||||
displayName: "VC Mod",
|
displayName: "VC Mod",
|
||||||
description: "Can manage voice chats",
|
description: "Can manage voice chats",
|
||||||
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
|
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
|
||||||
|
}, {
|
||||||
|
name: "CHAT_MODERATOR",
|
||||||
|
displayName: "Chat Mod",
|
||||||
|
description: "Can timeout people",
|
||||||
|
permissions: ["MODERATE_MEMBERS"]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const defaultSettings = Object.fromEntries(
|
const defaultSettings = Object.fromEntries(
|
||||||
|
@ -198,8 +204,7 @@ export default definePlugin({
|
||||||
replacement: [
|
replacement: [
|
||||||
// make the tag show the right text
|
// make the tag show the right text
|
||||||
{
|
{
|
||||||
// FIXME: Remove the BOT_TAG_BOT variant when the change arrives in stable
|
match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.APP_TAG/,
|
||||||
match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.(?:APP_TAG|BOT_TAG_BOT)/,
|
|
||||||
replace: (_, origSwitch, variant, tags, displayedText, strings) =>
|
replace: (_, origSwitch, variant, tags, displayedText, strings) =>
|
||||||
`${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}`
|
`${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}`
|
||||||
},
|
},
|
||||||
|
@ -264,34 +269,14 @@ export default definePlugin({
|
||||||
],
|
],
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (settings.store.tagSettings) return;
|
settings.store.tagSettings ??= defaultSettings;
|
||||||
// @ts-ignore
|
|
||||||
if (!settings.store.visibility_WEBHOOK) settings.store.tagSettings = defaultSettings;
|
// newly added field might be missing from old users
|
||||||
else {
|
settings.store.tagSettings.CHAT_MODERATOR ??= {
|
||||||
const newSettings = { ...defaultSettings };
|
text: "Chat Mod",
|
||||||
Object.entries(Vencord.PlainSettings.plugins.MoreUserTags).forEach(([name, value]) => {
|
showInChat: true,
|
||||||
const [setting, tag] = name.split("_");
|
showInNotChat: true
|
||||||
if (setting === "visibility") {
|
};
|
||||||
switch (value) {
|
|
||||||
case "always":
|
|
||||||
// its the default
|
|
||||||
break;
|
|
||||||
case "chat":
|
|
||||||
newSettings[tag].showInNotChat = false;
|
|
||||||
break;
|
|
||||||
case "not-chat":
|
|
||||||
newSettings[tag].showInChat = false;
|
|
||||||
break;
|
|
||||||
case "never":
|
|
||||||
newSettings[tag].showInChat = false;
|
|
||||||
newSettings[tag].showInNotChat = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settings.store.tagSettings = newSettings;
|
|
||||||
delete Vencord.Settings.plugins.MoreUserTags[name];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getPermissions(user: User, channel: Channel): string[] {
|
getPermissions(user: User, channel: Channel): string[] {
|
||||||
|
@ -322,20 +307,19 @@ export default definePlugin({
|
||||||
|
|
||||||
isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]),
|
isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]),
|
||||||
|
|
||||||
// FIXME: Remove the BOT_TAG_BOT variants from strings when the change arrives in stable
|
|
||||||
getTagText(passedTagName: string, strings: Record<string, string>) {
|
getTagText(passedTagName: string, strings: Record<string, string>) {
|
||||||
if (!passedTagName) return strings.APP_TAG ?? strings.BOT_TAG_BOT;
|
if (!passedTagName) return strings.APP_TAG;
|
||||||
const [tagName, variant] = passedTagName.split("-");
|
const [tagName, variant] = passedTagName.split("-");
|
||||||
const tag = tags.find(({ name }) => tagName === name);
|
const tag = tags.find(({ name }) => tagName === name);
|
||||||
if (!tag) return strings.APP_TAG ?? strings.BOT_TAG_BOT;
|
if (!tag) return strings.APP_TAG;
|
||||||
if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return strings.APP_TAG ?? strings.BOT_TAG_BOT;
|
if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return strings.APP_TAG;
|
||||||
|
|
||||||
const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName;
|
const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName;
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case "OP":
|
case "OP":
|
||||||
return `${strings.BOT_TAG_FORUM_ORIGINAL_POSTER} • ${tagText}`;
|
return `${strings.BOT_TAG_FORUM_ORIGINAL_POSTER} • ${tagText}`;
|
||||||
case "BOT":
|
case "BOT":
|
||||||
return `${strings.APP_TAG ?? strings.BOT_TAG_BOT} • ${tagText}`;
|
return `${strings.APP_TAG} • ${tagText}`;
|
||||||
default:
|
default:
|
||||||
return tagText;
|
return tagText;
|
||||||
}
|
}
|
||||||
|
@ -370,6 +354,15 @@ export default definePlugin({
|
||||||
if (location === "chat" && !settings.tagSettings[tag.name].showInChat) continue;
|
if (location === "chat" && !settings.tagSettings[tag.name].showInChat) continue;
|
||||||
if (location === "not-chat" && !settings.tagSettings[tag.name].showInNotChat) continue;
|
if (location === "not-chat" && !settings.tagSettings[tag.name].showInNotChat) continue;
|
||||||
|
|
||||||
|
// If the owner tag is disabled, and the user is the owner of the guild,
|
||||||
|
// avoid adding other tags because the owner will always match the condition for them
|
||||||
|
if (
|
||||||
|
tag.name !== "OWNER" &&
|
||||||
|
GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id &&
|
||||||
|
(location === "chat" && !settings.tagSettings.OWNER.showInChat) ||
|
||||||
|
(location === "not-chat" && !settings.tagSettings.OWNER.showInNotChat)
|
||||||
|
) continue;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
tag.permissions?.some(perm => perms.includes(perm)) ||
|
tag.permissions?.some(perm => perms.includes(perm)) ||
|
||||||
(tag.condition?.(message!, user, channel))
|
(tag.condition?.(message!, user, channel))
|
||||||
|
|
|
@ -16,6 +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 ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { isNonNullish } from "@utils/guards";
|
import { isNonNullish } from "@utils/guards";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
@ -55,12 +56,12 @@ export default definePlugin({
|
||||||
find: ".UserProfileSections.USER_INFO_CONNECTIONS:",
|
find: ".UserProfileSections.USER_INFO_CONNECTIONS:",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/,
|
match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/,
|
||||||
replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs($1,$2);"
|
replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs({user: $1, onClose: $2});"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
renderMutualGDMs(user: User, onClose: () => void) {
|
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
|
||||||
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
|
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
|
||||||
<Clickable
|
<Clickable
|
||||||
className={ProfileListClasses.listRow}
|
className={ProfileListClasses.listRow}
|
||||||
|
@ -99,5 +100,5 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
</ScrollerThin>
|
</ScrollerThin>
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 { definePluginSettings,migratePluginSettings } from "@api/Settings";
|
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
@ -31,6 +31,16 @@ const settings = definePluginSettings({
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
messages: {
|
||||||
|
description: "Server Notification Settings",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
options: [
|
||||||
|
{ label: "All messages", value: 0 },
|
||||||
|
{ label: "Only @mentions", value: 1 },
|
||||||
|
{ label: "Nothing", value: 2 },
|
||||||
|
{ label: "Server default", value: 3, default: true }
|
||||||
|
],
|
||||||
|
},
|
||||||
everyone: {
|
everyone: {
|
||||||
description: "Suppress @everyone and @here",
|
description: "Suppress @everyone and @here",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
|
@ -41,6 +51,16 @@ const settings = definePluginSettings({
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
highlights: {
|
||||||
|
description: "Suppress Highlights automatically",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
description: "Mute New Events automatically",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
showAllChannels: {
|
showAllChannels: {
|
||||||
description: "Show all channels automatically",
|
description: "Show all channels automatically",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
|
@ -53,7 +73,7 @@ export default definePlugin({
|
||||||
name: "NewGuildSettings",
|
name: "NewGuildSettings",
|
||||||
description: "Automatically mute new servers and change various other settings upon joining",
|
description: "Automatically mute new servers and change various other settings upon joining",
|
||||||
tags: ["MuteNewGuild", "mute", "server"],
|
tags: ["MuteNewGuild", "mute", "server"],
|
||||||
authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi],
|
authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi, Devs.GabiRP],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ",acceptInvite(",
|
find: ",acceptInvite(",
|
||||||
|
@ -78,8 +98,16 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
muted: settings.store.guild,
|
muted: settings.store.guild,
|
||||||
suppress_everyone: settings.store.everyone,
|
suppress_everyone: settings.store.everyone,
|
||||||
suppress_roles: settings.store.role
|
suppress_roles: settings.store.role,
|
||||||
|
mute_scheduled_events: settings.store.events,
|
||||||
|
notify_highlights: settings.store.highlights ? 1 : 0
|
||||||
});
|
});
|
||||||
|
if (settings.store.messages !== 3) {
|
||||||
|
updateGuildNotificationSettings(guildId,
|
||||||
|
{
|
||||||
|
message_notifications: settings.store.messages,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (settings.store.showAllChannels && isOptInEnabledForGuild(guildId)) {
|
if (settings.store.showAllChannels && isOptInEnabledForGuild(guildId)) {
|
||||||
toggleShowAllChannels(guildId);
|
toggleShowAllChannels(guildId);
|
||||||
}
|
}
|
||||||
|
|
5
src/plugins/noDefaultHangStatus/README.md
Normal file
5
src/plugins/noDefaultHangStatus/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# NoDefaultHangStatus
|
||||||
|
|
||||||
|
Disable the default hang status when joining voice channels
|
||||||
|
|
||||||
|

|
24
src/plugins/noDefaultHangStatus/index.ts
Normal file
24
src/plugins/noDefaultHangStatus/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "NoDefaultHangStatus",
|
||||||
|
description: "Disable the default hang status when joining voice channels",
|
||||||
|
authors: [Devs.D3SOX],
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "HangStatusTypes.CHILLING)",
|
||||||
|
replacement: {
|
||||||
|
match: /{enableHangStatus:(\i),/,
|
||||||
|
replace: "{_enableHangStatus:$1=false,"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
50
src/plugins/noServerEmojis/index.ts
Normal file
50
src/plugins/noServerEmojis/index.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
shownEmojis: {
|
||||||
|
description: "The types of emojis to show in the autocomplete menu.",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
default: "onlyUnicode",
|
||||||
|
options: [
|
||||||
|
{ label: "Only unicode emojis", value: "onlyUnicode" },
|
||||||
|
{ label: "Unicode emojis and server emojis from current server", value: "currentServer" },
|
||||||
|
{ label: "Unicode emojis and all server emojis (Discord default)", value: "all" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "NoServerEmojis",
|
||||||
|
authors: [Devs.UlyssesZhan],
|
||||||
|
description: "Do not show server emojis in the autocomplete menu.",
|
||||||
|
settings,
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "}searchWithoutFetchingLatest(",
|
||||||
|
replacement: {
|
||||||
|
match: /searchWithoutFetchingLatest.{20,300}get\((\i).{10,40}?reduce\(\((\i),(\i)\)=>\{/,
|
||||||
|
replace: "$& if ($self.shouldSkip($1, $3)) return $2;"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
shouldSkip(guildId: string, emoji: any) {
|
||||||
|
if (emoji.type !== "GUILD_EMOJI") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (settings.store.shownEmojis === "onlyUnicode") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (settings.store.shownEmojis === "currentServer") {
|
||||||
|
return emoji.guildId !== guildId;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
|
@ -26,6 +26,7 @@ const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
||||||
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/;
|
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/;
|
||||||
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
|
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
|
||||||
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
|
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
|
||||||
|
const TidalMatcher = /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/;
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
spotify: {
|
spotify: {
|
||||||
|
@ -42,6 +43,11 @@ const settings = definePluginSettings({
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Open Epic Games links in the Epic Games Launcher",
|
description: "Open Epic Games links in the Epic Games Launcher",
|
||||||
default: true,
|
default: true,
|
||||||
|
},
|
||||||
|
tidal: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Open Tidal links in the Tidal app",
|
||||||
|
default: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,7 +55,7 @@ const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative<typeof impo
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "OpenInApp",
|
name: "OpenInApp",
|
||||||
description: "Open Spotify, Steam and Epic Games URLs in their respective apps instead of your browser",
|
description: "Open Spotify, Tidal, Steam and Epic Games URLs in their respective apps instead of your browser",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
|
@ -127,6 +133,19 @@ export default definePlugin({
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tidal: {
|
||||||
|
if (!settings.store.tidal) break tidal;
|
||||||
|
|
||||||
|
const match = TidalMatcher.exec(url);
|
||||||
|
if (!match) break tidal;
|
||||||
|
|
||||||
|
const [, type, id] = match;
|
||||||
|
VencordNative.native.openExternal(`tidal://${type}/${id}`);
|
||||||
|
|
||||||
|
event?.preventDefault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// in case short url didn't end up being something we can handle
|
// in case short url didn't end up being something we can handle
|
||||||
if (event?.defaultPrevented) {
|
if (event?.defaultPrevented) {
|
||||||
window.open(url, "_blank");
|
window.open(url, "_blank");
|
||||||
|
|
|
@ -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 { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { FluxDispatcher } from "@webpack/common";
|
import { FluxDispatcher } from "@webpack/common";
|
||||||
|
@ -41,8 +41,9 @@ const settings = definePluginSettings({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
migratePluginSettings("PartyMode", "Party mode 🎉");
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Party mode 🎉",
|
name: "PartyMode",
|
||||||
description: "Allows you to use party mode cause the party never ends ✨",
|
description: "Allows you to use party mode cause the party never ends ✨",
|
||||||
authors: [Devs.UwUDev],
|
authors: [Devs.UwUDev],
|
||||||
settings,
|
settings,
|
||||||
|
|
5
src/plugins/pauseInvitesForever/README.md
Normal file
5
src/plugins/pauseInvitesForever/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# PauseInvitesForever
|
||||||
|
|
||||||
|
Adds a button to the Security Actions modal to to pause invites indefinitely.
|
||||||
|
|
||||||
|

|
77
src/plugins/pauseInvitesForever/index.tsx
Normal file
77
src/plugins/pauseInvitesForever/index.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { Constants, GuildStore, i18n, RestAPI } from "@webpack/common";
|
||||||
|
|
||||||
|
const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment");
|
||||||
|
|
||||||
|
function showDisableInvites(guildId: string) {
|
||||||
|
// Once the experiment is removed, this should keep working
|
||||||
|
const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
|
||||||
|
// @ts-ignore
|
||||||
|
return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableInvites(guildId: string) {
|
||||||
|
const guild = GuildStore.getGuild(guildId);
|
||||||
|
const features = [...guild.features, "INVITES_DISABLED"];
|
||||||
|
RestAPI.patch({
|
||||||
|
url: Constants.Endpoints.GUILD(guildId),
|
||||||
|
body: { features },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "PauseInvitesForever",
|
||||||
|
tags: ["DisableInvitesForever"],
|
||||||
|
description: "Brings back the option to pause invites indefinitely that stupit Discord removed.",
|
||||||
|
authors: [Devs.Dolfies, Devs.amia],
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION",
|
||||||
|
group: true,
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /children:\i\.\i\.\i\.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION/,
|
||||||
|
replace: "children: $self.renderInvitesLabel({guildId:arguments[0].guildId,setChecked})",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(\i\.hasDMsDisabled\)\(\i\),\[\i,(\i)\]=\i\.useState\(\i\))/,
|
||||||
|
replace: "$1,setChecked=$2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
renderInvitesLabel: ErrorBoundary.wrap(({ guildId, setChecked }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{i18n.Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION}
|
||||||
|
{showDisableInvites(guildId) && <a role="button" onClick={() => {
|
||||||
|
setChecked(true);
|
||||||
|
disableInvites(guildId);
|
||||||
|
}}> Pause Indefinitely.</a>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
});
|
|
@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
|
||||||
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
||||||
import { getUniqueUsername } from "@utils/discord";
|
import { getUniqueUsername } from "@utils/discord";
|
||||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
|
import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||||
import type { Guild } from "discord-types/general";
|
import type { Guild } from "discord-types/general";
|
||||||
|
|
||||||
import { settings } from "..";
|
import { settings } from "..";
|
||||||
|
@ -112,7 +112,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
||||||
<div
|
<div
|
||||||
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
|
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
|
||||||
onContextMenu={e => {
|
onContextMenu={e => {
|
||||||
if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role)
|
if (permission.type === PermissionType.Role)
|
||||||
ContextMenuApi.openContextMenu(e, () => (
|
ContextMenuApi.openContextMenu(e, () => (
|
||||||
<RoleContextMenu
|
<RoleContextMenu
|
||||||
guild={guild}
|
guild={guild}
|
||||||
|
@ -120,6 +120,14 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
||||||
onClose={modalProps.onClose}
|
onClose={modalProps.onClose}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
else if (permission.type === PermissionType.User) {
|
||||||
|
ContextMenuApi.openContextMenu(e, () => (
|
||||||
|
<UserContextMenu
|
||||||
|
userId={permission.id!}
|
||||||
|
onClose={modalProps.onClose}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
|
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
|
||||||
|
@ -200,24 +208,53 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
|
||||||
aria-label="Role Options"
|
aria-label="Role Options"
|
||||||
>
|
>
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
id="vc-pw-view-as-role"
|
id="vc-copy-role-id"
|
||||||
label="View As Role"
|
label={i18n.Messages.COPY_ID_ROLE}
|
||||||
action={() => {
|
action={() => {
|
||||||
const role = GuildStore.getRole(guild.id, roleId);
|
Clipboard.copy(roleId);
|
||||||
if (!role) return;
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
onClose();
|
{(settings.store as any).unsafeViewAsRole && (
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-pw-view-as-role"
|
||||||
|
label={i18n.Messages.VIEW_AS_ROLE}
|
||||||
|
action={() => {
|
||||||
|
const role = GuildStore.getRole(guild.id, roleId);
|
||||||
|
if (!role) return;
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
onClose();
|
||||||
type: "IMPERSONATE_UPDATE",
|
|
||||||
guildId: guild.id,
|
FluxDispatcher.dispatch({
|
||||||
data: {
|
type: "IMPERSONATE_UPDATE",
|
||||||
type: "ROLES",
|
guildId: guild.id,
|
||||||
roles: {
|
data: {
|
||||||
[roleId]: role
|
type: "ROLES",
|
||||||
|
roles: {
|
||||||
|
[roleId]: role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Menu.Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => void; }) {
|
||||||
|
return (
|
||||||
|
<Menu.Menu
|
||||||
|
navId={cl("user-context-menu")}
|
||||||
|
onClose={ContextMenuApi.closeContextMenu}
|
||||||
|
aria-label="User Options"
|
||||||
|
>
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-copy-user-id"
|
||||||
|
label={i18n.Messages.COPY_ID_USER}
|
||||||
|
action={() => {
|
||||||
|
Clipboard.copy(userId);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Menu.Menu>
|
</Menu.Menu>
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import ExpandableHeader from "@components/ExpandableHeader";
|
import { ExpandableHeader } from "@components/ExpandableHeader";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
|
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
|
||||||
import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/common";
|
import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/common";
|
||||||
|
|
|
@ -21,10 +21,9 @@ import { Devs } from "@utils/constants";
|
||||||
import { makeLazy } from "@utils/lazy";
|
import { makeLazy } from "@utils/lazy";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { UploadHandler, UserUtils } from "@webpack/common";
|
import { DraftType, UploadHandler, UploadManager, UserUtils } from "@webpack/common";
|
||||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||||
|
|
||||||
const DRAFT_TYPE = 0;
|
|
||||||
const DEFAULT_DELAY = 20;
|
const DEFAULT_DELAY = 20;
|
||||||
const DEFAULT_RESOLUTION = 128;
|
const DEFAULT_RESOLUTION = 128;
|
||||||
const FRAMES = 10;
|
const FRAMES = 10;
|
||||||
|
@ -59,9 +58,12 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
|
||||||
for (const opt of options) {
|
for (const opt of options) {
|
||||||
switch (opt.name) {
|
switch (opt.name) {
|
||||||
case "image":
|
case "image":
|
||||||
const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0];
|
const upload = UploadStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand);
|
||||||
if (upload) {
|
if (upload) {
|
||||||
if (!upload.isImage) throw "Upload is not an image";
|
if (!upload.isImage) {
|
||||||
|
UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
|
||||||
|
throw "Upload is not an image";
|
||||||
|
}
|
||||||
return upload.item.file;
|
return upload.item.file;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -73,10 +75,12 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
|
||||||
return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048");
|
return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[petpet] Failed to fetch user\n", err);
|
console.error("[petpet] Failed to fetch user\n", err);
|
||||||
|
UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
|
||||||
throw "Failed to fetch user. Check the console for more info.";
|
throw "Failed to fetch user. Check the console for more info.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,6 +134,7 @@ export default definePlugin({
|
||||||
var url = await resolveImage(opts, cmdCtx, noServerPfp);
|
var url = await resolveImage(opts, cmdCtx, noServerPfp);
|
||||||
if (!url) throw "No Image specified!";
|
if (!url) throw "No Image specified!";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
|
||||||
sendBotMessage(cmdCtx.channel.id, {
|
sendBotMessage(cmdCtx.channel.id, {
|
||||||
content: String(err),
|
content: String(err),
|
||||||
});
|
});
|
||||||
|
@ -147,6 +152,8 @@ export default definePlugin({
|
||||||
canvas.width = canvas.height = resolution;
|
canvas.width = canvas.height = resolution;
|
||||||
const ctx = canvas.getContext("2d")!;
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
|
||||||
|
UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
|
||||||
|
|
||||||
for (let i = 0; i < FRAMES; i++) {
|
for (let i = 0; i < FRAMES; i++) {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
@ -174,7 +181,7 @@ export default definePlugin({
|
||||||
const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" });
|
const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" });
|
||||||
// Immediately after the command finishes, Discord clears all input, including pending attachments.
|
// Immediately after the command finishes, Discord clears all input, including pending attachments.
|
||||||
// Thus, setTimeout is needed to make this execute after Discord cleared the input
|
// Thus, setTimeout is needed to make this execute after Discord cleared the input
|
||||||
setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10);
|
setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -83,7 +83,7 @@ export default definePlugin({
|
||||||
// Rendering
|
// Rendering
|
||||||
{
|
{
|
||||||
match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
|
match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
|
||||||
replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2);"
|
replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2)();"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /"renderSection",(\i)=>{/,
|
match: /"renderSection",(\i)=>{/,
|
||||||
|
@ -320,25 +320,26 @@ export default definePlugin({
|
||||||
</svg>
|
</svg>
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
}),
|
}, { noop: true }),
|
||||||
|
|
||||||
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
|
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
|
||||||
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
|
return ErrorBoundary.wrap(() => {
|
||||||
|
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
|
||||||
|
|
||||||
if (!channel || !category) return null;
|
if (!channel || !category) return null;
|
||||||
if (this.isChannelHidden(sectionIndex, index)) return null;
|
if (this.isChannelHidden(sectionIndex, index)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChannelComponent
|
<ChannelComponent
|
||||||
channel={channel}
|
channel={channel}
|
||||||
selected={this.instance.props.selectedChannelId === channel.id}
|
selected={this.instance.props.selectedChannelId === channel.id}
|
||||||
>
|
>
|
||||||
{channel.id}
|
{channel.id}
|
||||||
</ChannelComponent>
|
</ChannelComponent>
|
||||||
);
|
);
|
||||||
|
}, { noop: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
|
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
|
||||||
const category = categories[sectionIndex - 1];
|
const category = categories[sectionIndex - 1];
|
||||||
if (!category) return { channel: null, category: null };
|
if (!category) return { channel: null, category: null };
|
||||||
|
|
|
@ -33,7 +33,7 @@ const PRONOUN_TOOLTIP_PATCH = {
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "PronounDB",
|
name: "PronounDB",
|
||||||
authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven],
|
authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven, Devs.Elvyra],
|
||||||
description: "Adds pronouns to user messages using pronoundb",
|
description: "Adds pronouns to user messages using pronoundb",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { useAwaiter } from "@utils/react";
|
||||||
import { UserProfileStore, UserStore } from "@webpack/common";
|
import { UserProfileStore, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import { settings } from "./settings";
|
import { settings } from "./settings";
|
||||||
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
|
import { CachePronouns, PronounCode, PronounMapping, PronounsResponse } from "./types";
|
||||||
|
|
||||||
type PronounsWithSource = [string | null, string];
|
type PronounsWithSource = [string | null, string];
|
||||||
const EmptyPronouns: PronounsWithSource = [null, ""];
|
const EmptyPronouns: PronounsWithSource = [null, ""];
|
||||||
|
@ -40,9 +40,9 @@ export const enum PronounSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
// A map of cached pronouns so the same request isn't sent twice
|
// A map of cached pronouns so the same request isn't sent twice
|
||||||
const cache: Record<string, PronounCode> = {};
|
const cache: Record<string, CachePronouns> = {};
|
||||||
// A map of ids and callbacks that should be triggered on fetch
|
// A map of ids and callbacks that should be triggered on fetch
|
||||||
const requestQueue: Record<string, ((pronouns: PronounCode) => void)[]> = {};
|
const requestQueue: Record<string, ((pronouns: string) => void)[]> = {};
|
||||||
|
|
||||||
// Executes all queued requests and calls their callbacks
|
// Executes all queued requests and calls their callbacks
|
||||||
const bulkFetch = debounce(async () => {
|
const bulkFetch = debounce(async () => {
|
||||||
|
@ -50,7 +50,7 @@ const bulkFetch = debounce(async () => {
|
||||||
const pronouns = await bulkFetchPronouns(ids);
|
const pronouns = await bulkFetchPronouns(ids);
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
// Call all callbacks for the id
|
// Call all callbacks for the id
|
||||||
requestQueue[id]?.forEach(c => c(pronouns[id]));
|
requestQueue[id]?.forEach(c => c(pronouns[id] ? extractPronouns(pronouns[id].sets) : ""));
|
||||||
delete requestQueue[id];
|
delete requestQueue[id];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -78,8 +78,8 @@ export function useFormattedPronouns(id: string, useGlobalProfile: boolean = fal
|
||||||
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
|
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
|
||||||
return [discordPronouns, "Discord"];
|
return [discordPronouns, "Discord"];
|
||||||
|
|
||||||
if (result && result !== "unspecified")
|
if (result && result !== PronounMapping.unspecified)
|
||||||
return [formatPronouns(result), "PronounDB"];
|
return [result, "PronounDB"];
|
||||||
|
|
||||||
return [discordPronouns, "Discord"];
|
return [discordPronouns, "Discord"];
|
||||||
}
|
}
|
||||||
|
@ -98,8 +98,9 @@ const NewLineRe = /\n+/g;
|
||||||
|
|
||||||
// Gets the cached pronouns, if you're too impatient for a promise!
|
// Gets the cached pronouns, if you're too impatient for a promise!
|
||||||
export function getCachedPronouns(id: string): string | null {
|
export function getCachedPronouns(id: string): string | null {
|
||||||
const cached = cache[id];
|
const cached = cache[id] ? extractPronouns(cache[id].sets) : undefined;
|
||||||
if (cached && cached !== "unspecified") return cached;
|
|
||||||
|
if (cached && cached !== PronounMapping.unspecified) return cached;
|
||||||
|
|
||||||
return cached || null;
|
return cached || null;
|
||||||
}
|
}
|
||||||
|
@ -125,7 +126,7 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
|
||||||
params.append("ids", ids.join(","));
|
params.append("ids", ids.join(","));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const req = await fetch("https://pronoundb.org/api/v1/lookup-bulk?" + params.toString(), {
|
const req = await fetch("https://pronoundb.org/api/v2/lookup?" + params.toString(), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
|
@ -140,21 +141,24 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If the request errors, treat it as if no pronouns were found for all ids, and log it
|
// If the request errors, treat it as if no pronouns were found for all ids, and log it
|
||||||
console.error("PronounDB fetching failed: ", e);
|
console.error("PronounDB fetching failed: ", e);
|
||||||
const dummyPronouns = Object.fromEntries(ids.map(id => [id, "unspecified"] as const));
|
const dummyPronouns = Object.fromEntries(ids.map(id => [id, { sets: {} }] as const));
|
||||||
Object.assign(cache, dummyPronouns);
|
Object.assign(cache, dummyPronouns);
|
||||||
return dummyPronouns;
|
return dummyPronouns;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatPronouns(pronouns: string): string {
|
export function extractPronouns(pronounSet?: { [locale: string]: PronounCode[] }): string {
|
||||||
|
if (!pronounSet || !pronounSet.en) return PronounMapping.unspecified;
|
||||||
|
// PronounDB returns an empty set instead of {sets: {en: ["unspecified"]}}.
|
||||||
|
const pronouns = pronounSet.en;
|
||||||
const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; };
|
const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; };
|
||||||
// For capitalized pronouns, just return the mapping (it is by default capitalized)
|
|
||||||
if (pronounsFormat === PronounsFormat.Capitalized) return PronounMapping[pronouns];
|
if (pronouns.length === 1) {
|
||||||
// If it is set to lowercase and a special code (any, ask, avoid), then just return the capitalized text
|
// For capitalized pronouns or special codes (any, ask, avoid), we always return the normal (capitalized) string
|
||||||
else if (
|
if (pronounsFormat === PronounsFormat.Capitalized || ["any", "ask", "avoid", "other", "unspecified"].includes(pronouns[0]))
|
||||||
pronounsFormat === PronounsFormat.Lowercase
|
return PronounMapping[pronouns[0]];
|
||||||
&& ["any", "ask", "avoid", "other"].includes(pronouns)
|
else return PronounMapping[pronouns[0]].toLowerCase();
|
||||||
) return PronounMapping[pronouns];
|
}
|
||||||
// Otherwise (lowercase and not a special code), then convert the mapping to lowercase
|
const pronounString = pronouns.map(p => p[0].toUpperCase() + p.slice(1)).join("/");
|
||||||
else return PronounMapping[pronouns].toLowerCase();
|
return pronounsFormat === PronounsFormat.Capitalized ? pronounString : pronounString.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,31 +26,29 @@ export interface UserProfilePronounsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PronounsResponse {
|
export interface PronounsResponse {
|
||||||
[id: string]: PronounCode;
|
[id: string]: {
|
||||||
|
sets?: {
|
||||||
|
[locale: string]: PronounCode[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CachePronouns {
|
||||||
|
sets?: {
|
||||||
|
[locale: string]: PronounCode[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PronounCode = keyof typeof PronounMapping;
|
export type PronounCode = keyof typeof PronounMapping;
|
||||||
|
|
||||||
export const PronounMapping = {
|
export const PronounMapping = {
|
||||||
hh: "He/Him",
|
he: "He/Him",
|
||||||
hi: "He/It",
|
it: "It/Its",
|
||||||
hs: "He/She",
|
she: "She/Her",
|
||||||
ht: "He/They",
|
they: "They/Them",
|
||||||
ih: "It/Him",
|
|
||||||
ii: "It/Its",
|
|
||||||
is: "It/She",
|
|
||||||
it: "It/They",
|
|
||||||
shh: "She/He",
|
|
||||||
sh: "She/Her",
|
|
||||||
si: "She/It",
|
|
||||||
st: "She/They",
|
|
||||||
th: "They/He",
|
|
||||||
ti: "They/It",
|
|
||||||
ts: "They/She",
|
|
||||||
tt: "They/Them",
|
|
||||||
any: "Any pronouns",
|
any: "Any pronouns",
|
||||||
other: "Other pronouns",
|
other: "Other pronouns",
|
||||||
ask: "Ask me my pronouns",
|
ask: "Ask me my pronouns",
|
||||||
avoid: "Avoid pronouns, use my name",
|
avoid: "Avoid pronouns, use my name",
|
||||||
unspecified: "Unspecified"
|
unspecified: "No pronouns specified.",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue