mirror of
https://github.com/Vendicated/Vencord.git
synced 2024-09-20 06:30:35 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
0113739d63
114 changed files with 5107 additions and 2864 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
|
||||||
|
|
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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.7.9",
|
"version": "1.8.4",
|
||||||
"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": {
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
"zip-local": "^0.3.5",
|
"zip-local": "^0.3.5",
|
||||||
"zustand": "^3.7.2"
|
"zustand": "^3.7.2"
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
|
4435
pnpm-lock.yaml
4435
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -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();
|
||||||
|
|
|
@ -269,7 +269,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 +282,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
|
||||||
|
@ -310,45 +314,30 @@ 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>();
|
||||||
|
|
||||||
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>;
|
||||||
|
const lazyChunkRegex = canonicalizeMatch(/Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/g);
|
||||||
|
const chunkIdsRegex = canonicalizeMatch(/\("(.+?)"\)/g);
|
||||||
|
|
||||||
Object.defineProperty(Object.prototype, sym, {
|
async function searchAndLoadLazyChunks(factoryCode: string) {
|
||||||
get() {
|
const lazyChunks = factoryCode.matchAll(lazyChunkRegex);
|
||||||
chunks = this;
|
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
|
||||||
},
|
|
||||||
set() { },
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await (wreq as any).el(sym);
|
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
||||||
delete Object.prototype[sym];
|
const chunkIds = Array.from(rawChunkIds.matchAll(chunkIdsRegex)).map(m => m[1]);
|
||||||
|
if (chunkIds.length === 0) return;
|
||||||
|
|
||||||
const validChunksEntryPoints = new Set<string>();
|
let invalidChunkGroup = false;
|
||||||
const validChunks = new Set<string>();
|
|
||||||
const invalidChunks = new Set<string>();
|
|
||||||
|
|
||||||
if (!chunks) throw new Error("Failed to get chunks");
|
|
||||||
|
|
||||||
for (const entryPoint in chunks) {
|
|
||||||
const chunkIds = chunks[entryPoint];
|
|
||||||
let invalidEntryPoint = 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,56 +348,28 @@ 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
|
||||||
try {
|
await Promise.all(
|
||||||
// Loads all chunks required for an entry point
|
Array.from(validChunkGroups)
|
||||||
await (wreq as any).el(entryPoint);
|
.map(([chunkIds]) =>
|
||||||
} catch (err) { }
|
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
||||||
}
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Matches "id" or id:
|
// Requires the entry points for all valid chunk groups
|
||||||
const chunkIdRegex = /(?:"(\d+?)")|(?:(\d+?):)/g;
|
for (const [, entryPoint] of validChunkGroups) {
|
||||||
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 {
|
try {
|
||||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -416,54 +377,139 @@ function runTime(token: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
// 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);
|
process.exit(1);
|
||||||
|
@ -473,7 +519,7 @@ function runTime(token: string) {
|
||||||
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");
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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")}>
|
||||||
|
|
|
@ -180,7 +180,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 +189,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 +221,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 +235,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
||||||
setFullPatchError("");
|
setFullPatchError("");
|
||||||
|
|
||||||
setFind("");
|
setFind("");
|
||||||
|
setParsedFind("");
|
||||||
setMatch("");
|
setMatch("");
|
||||||
setReplacement("");
|
setReplacement("");
|
||||||
return;
|
return;
|
||||||
|
@ -256,7 +259,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 +270,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 +278,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>("");
|
||||||
|
|
||||||
|
@ -285,20 +290,34 @@ function PatchHelper() {
|
||||||
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);
|
setFindError(void 0);
|
||||||
setFind(v);
|
setFind(v);
|
||||||
if (v.length) {
|
}
|
||||||
findCandidates({ find: v, setModule, setError: setFindError });
|
|
||||||
|
function onFindBlur() {
|
||||||
|
try {
|
||||||
|
let parsedFind = find as string | RegExp;
|
||||||
|
if (/^\/.+?\/$/.test(find)) parsedFind = new RegExp(find.slice(1, -1));
|
||||||
|
|
||||||
|
setFindError(void 0);
|
||||||
|
setFind(find);
|
||||||
|
setParsedFind(parsedFind);
|
||||||
|
|
||||||
|
if (find.length) {
|
||||||
|
findCandidates({ find: parsedFind, setModule, setError: setFindError });
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setFindError((e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,19 +336,21 @@ 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}
|
||||||
onChange={onFindChange}
|
onChange={onFindChange}
|
||||||
|
onBlur={onFindBlur}
|
||||||
error={findError}
|
error={findError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Forms.FormTitle>match</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
||||||
<CheckedTextInput
|
<CheckedTextInput
|
||||||
value={match}
|
value={match}
|
||||||
onChange={onMatchChange}
|
onChange={onMatchChange}
|
||||||
|
@ -342,6 +363,7 @@ function PatchHelper() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className={Margins.top8} />
|
||||||
<ReplacementInput
|
<ReplacementInput
|
||||||
replacement={replacement}
|
replacement={replacement}
|
||||||
setReplacement={setReplacement}
|
setReplacement={setReplacement}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
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,72 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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 Canary
|
||||||
|
{
|
||||||
|
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 +92,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 +138,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 +221,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 })
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -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())}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
|
|
|
@ -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 { CustomEmoji } 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);
|
||||||
|
@ -111,7 +111,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 +129,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 +166,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 +190,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,
|
||||||
|
@ -397,6 +400,23 @@ 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;"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Allows the usage of subscription-locked emojis
|
||||||
|
{
|
||||||
|
find: "isUnusableRoleSubscriptionEmoji:function",
|
||||||
|
replacement: {
|
||||||
|
match: /isUnusableRoleSubscriptionEmoji:function/,
|
||||||
|
// replace the original export with a func that always returns false and alias the original
|
||||||
|
replace: "isUnusableRoleSubscriptionEmoji:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -413,31 +433,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 +806,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: CustomEmoji, channelId: string) {
|
||||||
if (e.require_colons === false) return true;
|
if (e.require_colons === false) 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 +927,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 +960,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);
|
||||||
|
|
||||||
|
|
|
@ -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, "", ""]
|
||||||
|
|
|
@ -9,9 +9,8 @@ import { Devs } from "@utils/constants";
|
||||||
import { getCurrentChannel } from "@utils/discord";
|
import { getCurrentChannel } from "@utils/discord";
|
||||||
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");
|
||||||
|
|
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";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,6 +34,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) {
|
||||||
|
@ -119,6 +123,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;
|
||||||
|
|
||||||
|
@ -138,7 +173,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +184,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]);
|
||||||
}
|
}
|
||||||
|
@ -182,7 +217,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +228,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";
|
||||||
|
@ -147,6 +148,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
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -313,6 +318,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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
![chat-view](https://github.com/Vendicated/Vencord/assets/82430093/69430881-60b3-422f-aa3d-c62953837566)
|
||||||
|
|
||||||
|
### Clock -ve Drift
|
||||||
|
|
||||||
|
![pissbot-on-top](https://github.com/Vendicated/Vencord/assets/82430093/d9248b66-e761-4872-8829-e8bf4fea6ec8)
|
||||||
|
|
||||||
|
### Clock +ve Drift
|
||||||
|
|
||||||
|
![dumb-ai](https://github.com/Vendicated/Vencord/assets/82430093/0e9783cf-51d5-4559-ae10-42399e7d4099)
|
||||||
|
|
||||||
|
### Connection Delay
|
||||||
|
|
||||||
|
![who-this](https://github.com/Vendicated/Vencord/assets/82430093/fd68873d-8630-42cc-a166-e9063d2718b2)
|
||||||
|
|
||||||
|
### Icons
|
||||||
|
|
||||||
|
![icons](https://github.com/Vendicated/Vencord/assets/82430093/17630bd9-44ee-4967-bcdf-3315eb6eca85)
|
193
src/plugins/messageLatency/index.tsx
Normal file
193
src/plugins/messageLatency/index.tsx
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DISCORD_KT_DELAY = 1471228.928;
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const diff: Diff = {
|
||||||
|
days: Math.round(delta / (60 * 60 * 24)),
|
||||||
|
hours: Math.round((delta / (60 * 60)) % 24),
|
||||||
|
minutes: Math.round((delta / (60)) % 60),
|
||||||
|
seconds: Math.round(delta % 60),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 !== ""
|
||||||
|
? k === "seconds"
|
||||||
|
? " and "
|
||||||
|
: " "
|
||||||
|
: "") + s
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
}, "");
|
||||||
|
|
||||||
|
return ts || "0 seconds";
|
||||||
|
},
|
||||||
|
|
||||||
|
latencyTooltipData(message: Message) {
|
||||||
|
const { latency, detectDiscordKotlin } = this.settings.store;
|
||||||
|
const { id, nonce } = message;
|
||||||
|
|
||||||
|
// Message wasn't received through gateway
|
||||||
|
if (!isNonNullish(nonce)) return null;
|
||||||
|
|
||||||
|
let isDiscordKotlin = false;
|
||||||
|
let delta = Math.round((SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce)) / 1000);
|
||||||
|
|
||||||
|
// Old Discord Android clients have a delay of around 17 days
|
||||||
|
// This is a workaround for that
|
||||||
|
if (-delta >= DISCORD_KT_DELAY - 86400) { // 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 stringDelta = abs >= latency ? this.stringDelta(abs) : null;
|
||||||
|
|
||||||
|
// Also thanks dziurwa
|
||||||
|
// 2 minutes
|
||||||
|
const TROLL_LIMIT = 2 * 60;
|
||||||
|
|
||||||
|
const fill: Fill = isDiscordKotlin
|
||||||
|
? ["status-positive", "status-positive", "text-muted"]
|
||||||
|
: delta >= TROLL_LIMIT || ahead
|
||||||
|
? ["text-muted", "text-muted", "text-muted"]
|
||||||
|
: delta >= (latency * 2)
|
||||||
|
? ["status-danger", "text-muted", "text-muted"]
|
||||||
|
: ["status-warning", "status-warning", "text-muted"];
|
||||||
|
|
||||||
|
return (abs >= latency || 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
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");
|
||||||
|
|
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.
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/47677887/e5ba40a3-cb08-462a-8615-fb74dd54c790)
|
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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
|
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
|
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
|
||||||
|
@ -64,7 +65,7 @@ export default definePlugin({
|
||||||
authors: [Devs.kemo],
|
authors: [Devs.kemo],
|
||||||
dependencies: ["ServerListAPI"],
|
dependencies: ["ServerListAPI"],
|
||||||
|
|
||||||
renderReadAllButton: () => <ReadAllButton />,
|
renderReadAllButton: ErrorBoundary.wrap(ReadAllButton, { noop: true }),
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
|
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
|
||||||
|
|
5
src/plugins/replyTimestamp/README.md
Normal file
5
src/plugins/replyTimestamp/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# ReplyTimestamp
|
||||||
|
|
||||||
|
Shows timestamps on the previews of replied-to messages. Pretty simple.
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/1547062/62e2b67a-e567-4c7a-884d-4640f897f7e0)
|
77
src/plugins/replyTimestamp/index.tsx
Normal file
77
src/plugins/replyTimestamp/index.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { Timestamp } from "@webpack/common";
|
||||||
|
import type { Message } from "discord-types/general";
|
||||||
|
import type { HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
const { getMessageTimestampId } = findByPropsLazy("getMessageTimestampId");
|
||||||
|
const { calendarFormat, dateFormat, isSameDay } = findByPropsLazy("calendarFormat", "dateFormat", "isSameDay", "accessibilityLabelCalendarFormat");
|
||||||
|
const MessageClasses = findByPropsLazy("separator", "latin24CompactTimeStamp");
|
||||||
|
|
||||||
|
function Sep(props: HTMLAttributes<HTMLElement>) {
|
||||||
|
return <i className={MessageClasses.separator} aria-hidden={true} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum ReferencedMessageState {
|
||||||
|
LOADED = 0,
|
||||||
|
NOT_LOADED = 1,
|
||||||
|
DELETED = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReferencedMessage = { state: ReferencedMessageState.LOADED; message: Message; } | { state: ReferencedMessageState.NOT_LOADED | ReferencedMessageState.DELETED; };
|
||||||
|
|
||||||
|
function ReplyTimestamp({
|
||||||
|
referencedMessage,
|
||||||
|
baseMessage,
|
||||||
|
}: {
|
||||||
|
referencedMessage: ReferencedMessage,
|
||||||
|
baseMessage: Message;
|
||||||
|
}) {
|
||||||
|
if (referencedMessage.state !== ReferencedMessageState.LOADED) return null;
|
||||||
|
const refTimestamp = referencedMessage.message.timestamp as any;
|
||||||
|
const baseTimestamp = baseMessage.timestamp as any;
|
||||||
|
return (
|
||||||
|
<Timestamp
|
||||||
|
id={getMessageTimestampId(referencedMessage.message)}
|
||||||
|
className="vc-reply-timestamp"
|
||||||
|
compact={isSameDay(refTimestamp, baseTimestamp)}
|
||||||
|
timestamp={refTimestamp}
|
||||||
|
isInline={false}
|
||||||
|
>
|
||||||
|
<Sep>[</Sep>
|
||||||
|
{isSameDay(refTimestamp, baseTimestamp)
|
||||||
|
? dateFormat(refTimestamp, "LT")
|
||||||
|
: calendarFormat(refTimestamp)
|
||||||
|
}
|
||||||
|
<Sep>]</Sep>
|
||||||
|
</Timestamp>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ReplyTimestamp",
|
||||||
|
description: "Shows a timestamp on replied-message previews",
|
||||||
|
authors: [Devs.Kyuuhachi],
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "renderSingleLineMessage:function()",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<="aria-label":\i,children:\[)(?=\i,\i,\i\])/,
|
||||||
|
replace: "$self.ReplyTimestamp(arguments[0]),"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
ReplyTimestamp: ErrorBoundary.wrap(ReplyTimestamp, { noop: true }),
|
||||||
|
});
|
3
src/plugins/replyTimestamp/style.css
Normal file
3
src/plugins/replyTimestamp/style.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.vc-reply-timestamp {
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
|
@ -91,7 +91,7 @@ const settings = definePluginSettings({
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ResurrectHome",
|
name: "ResurrectHome",
|
||||||
description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking the Server Guide.",
|
description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking a server.",
|
||||||
authors: [Devs.Dolfies, Devs.Nuckyz],
|
authors: [Devs.Dolfies, Devs.Nuckyz],
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ export default definePlugin({
|
||||||
find: "487e85_1",
|
find: "487e85_1",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=text:(\i)\?\i\.\i\.Messages\.SERVER_GUIDE:\i\.\i\.Messages\.GUILD_HOME,)/,
|
match: /(?<=text:(\i)\?\i\.\i\.Messages\.SERVER_GUIDE:\i\.\i\.Messages\.GUILD_HOME,)/,
|
||||||
replace: "badge:$self.ViewServerHomeButton({serverGuide:$1}),"
|
replace: "trailing:$self.ViewServerHomeButton({serverGuide:$1}),"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Disable view Server Home override when the Server Home is unmouted
|
// Disable view Server Home override when the Server Home is unmouted
|
||||||
|
|
|
@ -23,7 +23,6 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import ExpandableHeader from "@components/ExpandableHeader";
|
import ExpandableHeader from "@components/ExpandableHeader";
|
||||||
import { OpenExternalIcon } from "@components/Icons";
|
import { OpenExternalIcon } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { Logger } from "@utils/Logger";
|
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Alerts, Menu, Parser, useState } from "@webpack/common";
|
import { Alerts, Menu, Parser, useState } from "@webpack/common";
|
||||||
import { Guild, User } from "discord-types/general";
|
import { Guild, User } from "discord-types/general";
|
||||||
|
@ -36,13 +35,26 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
|
||||||
import { settings } from "./settings";
|
import { settings } from "./settings";
|
||||||
import { showToast } from "./utils";
|
import { showToast } from "./utils";
|
||||||
|
|
||||||
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => {
|
const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => {
|
||||||
|
if (!guild) return;
|
||||||
children.push(
|
children.push(
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
label="View Reviews"
|
label="View Reviews"
|
||||||
id="vc-rdb-server-reviews"
|
id="vc-rdb-server-reviews"
|
||||||
icon={OpenExternalIcon}
|
icon={OpenExternalIcon}
|
||||||
action={() => openReviewsModal(props.guild.id, props.guild.name)}
|
action={() => openReviewsModal(guild.id, guild.name)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const userContextPatch: NavContextMenuPatchCallback = (children, { user }: { user?: User, onClose(): void; }) => {
|
||||||
|
if (!user) return;
|
||||||
|
children.push(
|
||||||
|
<Menu.MenuItem
|
||||||
|
label="View Reviews"
|
||||||
|
id="vc-rdb-user-reviews"
|
||||||
|
icon={OpenExternalIcon}
|
||||||
|
action={() => openReviewsModal(user.id, user.username)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -54,7 +66,10 @@ export default definePlugin({
|
||||||
|
|
||||||
settings,
|
settings,
|
||||||
contextMenus: {
|
contextMenus: {
|
||||||
"guild-header-popout": guildPopoutPatch
|
"guild-header-popout": guildPopoutPatch,
|
||||||
|
"guild-context": guildPopoutPatch,
|
||||||
|
"user-context": userContextPatch,
|
||||||
|
"user-profile-actions": userContextPatch
|
||||||
},
|
},
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
|
@ -75,13 +90,6 @@ export default definePlugin({
|
||||||
const s = settings.store;
|
const s = settings.store;
|
||||||
const { lastReviewId, notifyReviews } = s;
|
const { lastReviewId, notifyReviews } = s;
|
||||||
|
|
||||||
const legacy = s as any as { token?: string; };
|
|
||||||
if (legacy.token) {
|
|
||||||
await updateAuth({ token: legacy.token });
|
|
||||||
legacy.token = undefined;
|
|
||||||
new Logger("ReviewDB").info("Migrated legacy settings");
|
|
||||||
}
|
|
||||||
|
|
||||||
await initAuth();
|
await initAuth();
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
|
|
@ -94,7 +94,7 @@ export default definePlugin({
|
||||||
find: "renderPrioritySpeaker",
|
find: "renderPrioritySpeaker",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /renderName\(\).{0,100}speaking:.+?\.clanTag.+?"div",{/,
|
match: /renderName\(\){.+?usernameSpeaking\]:.+?(?=children)/,
|
||||||
replace: "$&...$self.getVoiceProps(this.props),"
|
replace: "$&...$self.getVoiceProps(this.props),"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -14,10 +14,11 @@ export default definePlugin({
|
||||||
authors: [Devs.AndrewDLO, Devs.FieryFlames],
|
authors: [Devs.AndrewDLO, Devs.FieryFlames],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "call_ringing_beat\"",
|
find: '"call_ringing_beat"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /500===\i\(\)\.random\(1,1e3\)/,
|
// FIXME Remove === alternative when it hits stable
|
||||||
replace: "true"
|
match: /500(!==|===)\i\(\)\.random\(1,1e3\)/,
|
||||||
|
replace: (_, predicate) => predicate === "!==" ? "false" : "true",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -36,6 +36,8 @@ const enum ShowMode {
|
||||||
HiddenIconWithMutedStyle
|
HiddenIconWithMutedStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONNECT = 1n << 20n;
|
||||||
|
|
||||||
export const settings = definePluginSettings({
|
export const settings = definePluginSettings({
|
||||||
hideUnreads: {
|
hideUnreads: {
|
||||||
description: "Hide Unreads",
|
description: "Hide Unreads",
|
||||||
|
@ -273,12 +275,12 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
// Change the role permission check to CONNECT if the channel is locked
|
// Change the role permission check to CONNECT if the channel is locked
|
||||||
match: /ADMINISTRATOR\)\|\|(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,
|
match: /ADMINISTRATOR\)\|\|(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,
|
||||||
replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${PermissionsBits.CONNECT}n,${channel})?${permCheck}CONNECT):`
|
replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Change the permissionOverwrite check to CONNECT if the channel is locked
|
// Change the permissionOverwrite check to CONNECT if the channel is locked
|
||||||
match: /permissionOverwrites\[.+?\i=(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,
|
match: /permissionOverwrites\[.+?\i=(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,
|
||||||
replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${PermissionsBits.CONNECT}n,${channel})?${permCheck}CONNECT):`
|
replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Include the @everyone role in the allowed roles list for Hidden Channels
|
// Include the @everyone role in the allowed roles list for Hidden Channels
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# ShowHiddenThings
|
# ShowHiddenThings
|
||||||
|
|
||||||
Displays various moderator-only elements regardless of permissions.
|
Displays various hidden & moderator-only things regardless of permissions.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -9,3 +9,11 @@ Displays various moderator-only elements regardless of permissions.
|
||||||
|
|
||||||
- Show the invites paused tooltip in the server list
|
- Show the invites paused tooltip in the server list
|
||||||
![](https://github.com/Vendicated/Vencord/assets/47677887/b6a923d2-ac55-40d9-b4f8-fa6fc117148b)
|
![](https://github.com/Vendicated/Vencord/assets/47677887/b6a923d2-ac55-40d9-b4f8-fa6fc117148b)
|
||||||
|
|
||||||
|
- Show the member mod view context menu item in all servers
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/47677887/3dac95dd-841c-4c15-ad87-2db7bd1e4dab)
|
||||||
|
|
||||||
|
- Disable filters in Server Discovery search that hide servers that don't meet discovery criteria
|
||||||
|
|
||||||
|
- Disable filters in Server Discovery search that hide NSFW & disallowed servers
|
||||||
|
|
|
@ -31,13 +31,28 @@ const settings = definePluginSettings({
|
||||||
description: "Show the invites paused tooltip in the server list.",
|
description: "Show the invites paused tooltip in the server list.",
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
showModView: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Show the member mod view context menu item in all servers.",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
disableDiscoveryFilters: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Disable filters in Server Discovery search that hide servers that don't meet discovery criteria.",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
disableDisallowedDiscoveryFilters: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Disable filters in Server Discovery search that hide NSFW & disallowed servers.",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
|
migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ShowHiddenThings",
|
name: "ShowHiddenThings",
|
||||||
tags: ["ShowTimeouts", "ShowInvitesPaused"],
|
tags: ["ShowTimeouts", "ShowInvitesPaused", "ShowModView", "DisableDiscoveryFilters"],
|
||||||
description: "Displays various moderator-only elements regardless of permissions.",
|
description: "Displays various hidden & moderator-only things regardless of permissions.",
|
||||||
authors: [Devs.Dolfies],
|
authors: [Devs.Dolfies],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
|
@ -55,6 +70,39 @@ export default definePlugin({
|
||||||
match: /\i\.\i\.can\(\i\.Permissions.MANAGE_GUILD,\i\)/,
|
match: /\i\.\i\.can\(\i\.Permissions.MANAGE_GUILD,\i\)/,
|
||||||
replace: "true",
|
replace: "true",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "canAccessGuildMemberModViewWithExperiment:",
|
||||||
|
predicate: () => settings.store.showModView,
|
||||||
|
replacement: {
|
||||||
|
match: /return \i\.hasAny\(\i\.computePermissions\(\{user:\i,context:\i,checkElevated:!1\}\),\i\.MemberSafetyPagePermissions\)/,
|
||||||
|
replace: "return true",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "auto_removed:",
|
||||||
|
predicate: () => settings.store.disableDiscoveryFilters,
|
||||||
|
replacement: {
|
||||||
|
match: /filters:\i\.join\(" AND "\),facets:\[/,
|
||||||
|
replace: "facets:["
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "DiscoveryBannedSearchWords.includes",
|
||||||
|
predicate: () => settings.store.disableDisallowedDiscoveryFilters,
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=function\(\){)(?=.{0,130}DiscoveryBannedSearchWords\.includes)/,
|
||||||
|
replace: "return false;"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "Endpoints.GUILD_DISCOVERY_VALID_TERM",
|
||||||
|
predicate: () => settings.store.disableDisallowedDiscoveryFilters,
|
||||||
|
all: true,
|
||||||
|
replacement: {
|
||||||
|
match: /\i\.HTTP\.get\(\{url:\i\.Endpoints\.GUILD_DISCOVERY_VALID_TERM,query:\{term:\i\},oldFormErrors:!0\}\);/g,
|
||||||
|
replace: "Promise.resolve({ body: { valid: true } });"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
settings,
|
settings,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
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 { Message, User } from "discord-types/general";
|
import { Message, User } from "discord-types/general";
|
||||||
|
@ -56,7 +57,7 @@ export default definePlugin({
|
||||||
],
|
],
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
|
renderUsername: ErrorBoundary.wrap(({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
|
||||||
try {
|
try {
|
||||||
const user = userOverride ?? message.author;
|
const user = userOverride ?? message.author;
|
||||||
let { username } = user;
|
let { username } = user;
|
||||||
|
@ -66,14 +67,14 @@ export default definePlugin({
|
||||||
const { nick } = author;
|
const { nick } = author;
|
||||||
const prefix = withMentionPrefix ? "@" : "";
|
const prefix = withMentionPrefix ? "@" : "";
|
||||||
if (username === nick || isRepliedMessage && !settings.store.inReplies)
|
if (username === nick || isRepliedMessage && !settings.store.inReplies)
|
||||||
return prefix + nick;
|
return <>{prefix}{nick}</>;
|
||||||
if (settings.store.mode === "user-nick")
|
if (settings.store.mode === "user-nick")
|
||||||
return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>;
|
return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>;
|
||||||
if (settings.store.mode === "nick-user")
|
if (settings.store.mode === "nick-user")
|
||||||
return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>;
|
return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>;
|
||||||
return prefix + username;
|
return <>{prefix}{username}</>;
|
||||||
} catch {
|
} catch {
|
||||||
return author?.nick;
|
return <>{author?.nick}</>;
|
||||||
}
|
}
|
||||||
},
|
}, { noop: true }),
|
||||||
});
|
});
|
||||||
|
|
8
src/plugins/showTimeoutDuration/README.md
Normal file
8
src/plugins/showTimeoutDuration/README.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# ShowTimeoutDuration
|
||||||
|
|
||||||
|
Displays how much longer a user's timeout will last.
|
||||||
|
Either in the timeout icon tooltip, or next to it, configurable via settings!
|
||||||
|
|
||||||
|
![indicator in tooltip](https://github.com/Vendicated/Vencord/assets/45497981/606588a3-2646-40d9-8800-b6307f650136)
|
||||||
|
|
||||||
|
![indicator next to timeout icon](https://github.com/Vendicated/Vencord/assets/45497981/ab9d2101-0fdc-4143-9310-9488f056eeee)
|
106
src/plugins/showTimeoutDuration/index.tsx
Normal file
106
src/plugins/showTimeoutDuration/index.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { findComponentLazy } from "@webpack";
|
||||||
|
import { ChannelStore, Forms, GuildMemberStore, i18n, Text, Tooltip } from "@webpack/common";
|
||||||
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER"));
|
||||||
|
|
||||||
|
const enum DisplayStyle {
|
||||||
|
Tooltip = "tooltip",
|
||||||
|
Inline = "ssalggnikool"
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
displayStyle: {
|
||||||
|
description: "How to display the timeout duration",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
restartNeeded: true,
|
||||||
|
options: [
|
||||||
|
{ label: "In the Tooltip", value: DisplayStyle.Tooltip },
|
||||||
|
{ label: "Next to the timeout icon", value: DisplayStyle.Inline, default: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderTimeout(message: Message, inline: boolean) {
|
||||||
|
const guildId = ChannelStore.getChannel(message.channel_id)?.guild_id;
|
||||||
|
if (!guildId) return null;
|
||||||
|
|
||||||
|
const member = GuildMemberStore.getMember(guildId, message.author.id);
|
||||||
|
if (!member?.communicationDisabledUntil) return null;
|
||||||
|
|
||||||
|
const countdown = () => (
|
||||||
|
<CountDown
|
||||||
|
deadline={new Date(member.communicationDisabledUntil!)}
|
||||||
|
showUnits
|
||||||
|
stopAtOneSec
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return inline
|
||||||
|
? countdown()
|
||||||
|
: i18n.Messages.GUILD_ENABLE_COMMUNICATION_TIME_REMAINING.format({
|
||||||
|
username: message.author.username,
|
||||||
|
countdown
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ShowTimeoutDuration",
|
||||||
|
description: "Shows how much longer a user's timeout will last, either in the timeout icon tooltip or next to it",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /(\i)\.Tooltip,{(text:.{0,30}\.Messages\.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY)/,
|
||||||
|
get replace() {
|
||||||
|
if (settings.store.displayStyle === DisplayStyle.Inline)
|
||||||
|
return "$self.TooltipWrapper,{vcProps:arguments[0],$2";
|
||||||
|
|
||||||
|
return "$1.Tooltip,{text:$self.renderTimeoutDuration(arguments[0])";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
renderTimeoutDuration: ErrorBoundary.wrap(({ message }: { message: Message; }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormText>{i18n.Messages.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}</Forms.FormText>
|
||||||
|
<Forms.FormText className={Margins.top8}>
|
||||||
|
{renderTimeout(message, false)}
|
||||||
|
</Forms.FormText>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, { noop: true }),
|
||||||
|
|
||||||
|
TooltipWrapper: ErrorBoundary.wrap(({ vcProps: { message }, ...tooltipProps }: { vcProps: { message: Message; }; }) => {
|
||||||
|
return (
|
||||||
|
<div className="vc-std-wrapper">
|
||||||
|
<Tooltip {...tooltipProps as any} />
|
||||||
|
|
||||||
|
<Text variant="text-md/normal" color="status-danger">
|
||||||
|
{renderTimeout(message, true)} timeout remaining
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, { noop: true })
|
||||||
|
});
|
4
src/plugins/showTimeoutDuration/styles.css
Normal file
4
src/plugins/showTimeoutDuration/styles.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.vc-std-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
|
@ -18,10 +18,11 @@
|
||||||
|
|
||||||
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
|
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
|
||||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
|
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
|
||||||
|
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
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 { FluxDispatcher, React } from "@webpack/common";
|
import { FluxDispatcher, Menu, React } from "@webpack/common";
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
showIcon: {
|
showIcon: {
|
||||||
|
@ -30,6 +31,11 @@ const settings = definePluginSettings({
|
||||||
description: "Show an icon for toggling the plugin",
|
description: "Show an icon for toggling the plugin",
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
},
|
},
|
||||||
|
contextMenu: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Add option to toggle the functionality in the chat input context menu",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
isEnabled: {
|
isEnabled: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Toggle functionality",
|
description: "Toggle functionality",
|
||||||
|
@ -56,13 +62,37 @@ const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => {
|
||||||
|
const { isEnabled, contextMenu } = settings.use(["isEnabled", "contextMenu"]);
|
||||||
|
if (!contextMenu) return;
|
||||||
|
|
||||||
|
const group = findGroupChildrenByChildId("submit-button", children);
|
||||||
|
|
||||||
|
if (!group) return;
|
||||||
|
|
||||||
|
const idx = group.findIndex(c => c?.props?.id === "submit-button");
|
||||||
|
|
||||||
|
group.splice(idx + 1, 0,
|
||||||
|
<Menu.MenuCheckboxItem
|
||||||
|
id="vc-silent-typing"
|
||||||
|
label="Enable Silent Typing"
|
||||||
|
checked={isEnabled}
|
||||||
|
action={() => settings.store.isEnabled = !settings.store.isEnabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "SilentTyping",
|
name: "SilentTyping",
|
||||||
authors: [Devs.Ven, Devs.Rini],
|
authors: [Devs.Ven, Devs.Rini, Devs.ImBanana],
|
||||||
description: "Hide that you are typing",
|
description: "Hide that you are typing",
|
||||||
dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
|
dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
|
||||||
settings,
|
settings,
|
||||||
|
contextMenus: {
|
||||||
|
"textarea-context": ChatBarContextCheckbox
|
||||||
|
},
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '.dispatch({type:"TYPING_START_LOCAL"',
|
find: '.dispatch({type:"TYPING_START_LOCAL"',
|
||||||
|
|
|
@ -41,8 +41,8 @@ export default definePlugin({
|
||||||
patches: [{
|
patches: [{
|
||||||
find: "getRelationshipCounts(){",
|
find: "getRelationshipCounts(){",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\.sortBy\(\i=>\i\.comparator\)/,
|
match: /\}\)\.sortBy\((.+?)\)\.value\(\)/,
|
||||||
replace: "$&.sortBy((row) => $self.sortList(row))"
|
replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()"
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
find: ".Messages.FRIEND_REQUEST_CANCEL",
|
find: ".Messages.FRIEND_REQUEST_CANCEL",
|
||||||
|
@ -53,10 +53,10 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
|
||||||
sortList(row: any) {
|
wrapSort(comparator: Function, row: any) {
|
||||||
return row.type === 3 || row.type === 4
|
return row.type === 3 || row.type === 4
|
||||||
? -this.getSince(row.user)
|
? -this.getSince(row.user)
|
||||||
: row.comparator;
|
: comparator(row);
|
||||||
},
|
},
|
||||||
|
|
||||||
getSince(user: User) {
|
getSince(user: User) {
|
||||||
|
|
|
@ -26,10 +26,12 @@ export default definePlugin({
|
||||||
description: "Adds Startup Timings to the Settings menu",
|
description: "Adds Startup Timings to the Settings menu",
|
||||||
authors: [Devs.Megu],
|
authors: [Devs.Megu],
|
||||||
patches: [{
|
patches: [{
|
||||||
find: "UserSettingsSections.PAYMENT_FLOW_MODAL_TEST_PAGE,",
|
find: "Messages.ACTIVITY_SETTINGS",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /{section:\i\.UserSettingsSections\.PAYMENT_FLOW_MODAL_TEST_PAGE/,
|
match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\))/,
|
||||||
replace: '{section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage},$&'
|
replace: (_, commaOrSemi, settings, elements) => "" +
|
||||||
|
`${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
|
||||||
|
`&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
StartupTimingPage
|
StartupTimingPage
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# ThemeAttributes
|
# ThemeAttributes
|
||||||
|
|
||||||
This plugin adds data attributes to various elements inside Discord
|
This plugin adds data attributes and CSS variables to various elements inside Discord
|
||||||
|
|
||||||
This allows themes to more easily theme those elements or even do things that otherwise wouldn't be possible
|
This allows themes to more easily theme those elements or even do things that otherwise wouldn't be possible
|
||||||
|
|
||||||
|
@ -15,6 +15,15 @@ This allows themes to more easily theme those elements or even do things that ot
|
||||||
### Chat Messages
|
### Chat Messages
|
||||||
|
|
||||||
- `data-author-id` contains the id of the author
|
- `data-author-id` contains the id of the author
|
||||||
|
- `data-author-username` contains the username of the author
|
||||||
- `data-is-self` is a boolean indicating whether this is the current user's message
|
- `data-is-self` is a boolean indicating whether this is the current user's message
|
||||||
|
|
||||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)
|
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)
|
||||||
|
|
||||||
|
## CSS Variables
|
||||||
|
|
||||||
|
### Avatars
|
||||||
|
|
||||||
|
`--avatar-url-<resolution>` contains a URL for the users avatar with the size attribute adjusted for the resolutions `128, 256, 512, 1024, 2048, 4096`.
|
||||||
|
|
||||||
|
![image](https://github.com/Vendicated/Vencord/assets/26598490/192ddac0-c827-472f-9933-fa99ff36f723)
|
||||||
|
|
|
@ -9,10 +9,11 @@ import definePlugin from "@utils/types";
|
||||||
import { UserStore } from "@webpack/common";
|
import { UserStore } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ThemeAttributes",
|
name: "ThemeAttributes",
|
||||||
description: "Adds data attributes to various elements for theming purposes",
|
description: "Adds data attributes to various elements for theming purposes",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven, Devs.Board],
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
// Add data-tab-id to all tab bar items
|
// Add data-tab-id to all tab bar items
|
||||||
|
@ -32,14 +33,43 @@ export default definePlugin({
|
||||||
match: /\.messageListItem(?=,"aria)/,
|
match: /\.messageListItem(?=,"aria)/,
|
||||||
replace: "$&,...$self.getMessageProps(arguments[0])"
|
replace: "$&,...$self.getMessageProps(arguments[0])"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// add --avatar-url-<resolution> css variable to avatar img elements
|
||||||
|
// popout profiles
|
||||||
|
{
|
||||||
|
find: ".LABEL_WITH_ONLINE_STATUS",
|
||||||
|
replacement: {
|
||||||
|
match: /src:null!=\i\?(\i).{1,50}"aria-hidden":!0/,
|
||||||
|
replace: "$&,style:$self.getAvatarStyles($1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// chat avatars
|
||||||
|
{
|
||||||
|
find: "showCommunicationDisabledStyles",
|
||||||
|
replacement: {
|
||||||
|
match: /src:(\i),"aria-hidden":!0/,
|
||||||
|
replace: "$&,style:$self.getAvatarStyles($1)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
getAvatarStyles(src: string) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
[128, 256, 512, 1024, 2048, 4096].map(size => [
|
||||||
|
`--avatar-url-${size}`,
|
||||||
|
`url(${src.replace(/\d+$/, String(size))})`
|
||||||
|
])
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
getMessageProps(props: { message: Message; }) {
|
getMessageProps(props: { message: Message; }) {
|
||||||
const authorId = props.message?.author?.id;
|
const author = props.message?.author;
|
||||||
|
const authorId = author?.id;
|
||||||
return {
|
return {
|
||||||
"data-author-id": authorId,
|
"data-author-id": authorId,
|
||||||
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id
|
"data-author-username": author?.username,
|
||||||
|
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,9 +40,9 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?:
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
|
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
|
||||||
const { autoTranslate } = settings.use(["autoTranslate"]);
|
const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]);
|
||||||
|
|
||||||
if (!isMainChat) return null;
|
if (!isMainChat || !showChatBarButton) return null;
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
const newState = !autoTranslate;
|
const newState = !autoTranslate;
|
||||||
|
|
|
@ -48,6 +48,11 @@ export const settings = definePluginSettings({
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
|
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
showChatBarButton: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Show translate button in chat bar",
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
}).withPrivateSettings<{
|
}).withPrivateSettings<{
|
||||||
showAutoTranslateAlert: boolean;
|
showAutoTranslateAlert: boolean;
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/Co
|
||||||
import { ImageInvisible, ImageVisible } from "@components/Icons";
|
import { ImageInvisible, ImageVisible } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
|
import { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
const EMBED_SUPPRESSED = 1 << 2;
|
const EMBED_SUPPRESSED = 1 << 2;
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channe
|
||||||
icon={isEmbedSuppressed ? ImageVisible : ImageInvisible}
|
icon={isEmbedSuppressed ? ImageVisible : ImageInvisible}
|
||||||
action={() =>
|
action={() =>
|
||||||
RestAPI.patch({
|
RestAPI.patch({
|
||||||
url: `/channels/${channel.id}/messages/${messageId}`,
|
url: Constants.Endpoints.MESSAGE(channel.id, messageId),
|
||||||
body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED }
|
body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,9 +24,14 @@ import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
import style from "./index.css?managed";
|
import style from "./index.css?managed";
|
||||||
|
|
||||||
const BASE_URL = "https://raw.githubusercontent.com/AutumnVN/usrbg/main/usrbg.json";
|
const API_URL = "https://usrbg.is-hardly.online/users";
|
||||||
|
|
||||||
let data = {} as Record<string, string>;
|
interface UsrbgApiReturn {
|
||||||
|
endpoint: string
|
||||||
|
bucket: string
|
||||||
|
prefix: string
|
||||||
|
users: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
nitroFirst: {
|
nitroFirst: {
|
||||||
|
@ -48,7 +53,7 @@ const settings = definePluginSettings({
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "USRBG",
|
name: "USRBG",
|
||||||
description: "Displays user banners from USRBG, allowing anyone to get a banner without Nitro",
|
description: "Displays user banners from USRBG, allowing anyone to get a banner without Nitro",
|
||||||
authors: [Devs.AutumnVN, Devs.pylix, Devs.TheKodeToad],
|
authors: [Devs.AutumnVN, Devs.katlyn, Devs.pylix, Devs.TheKodeToad],
|
||||||
settings,
|
settings,
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
|
@ -80,8 +85,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
data: null as UsrbgApiReturn | null,
|
||||||
data,
|
|
||||||
|
|
||||||
settingsAboutComponent: () => {
|
settingsAboutComponent: () => {
|
||||||
return (
|
return (
|
||||||
|
@ -91,9 +95,9 @@ export default definePlugin({
|
||||||
|
|
||||||
voiceBackgroundHook({ className, participantUserId }: any) {
|
voiceBackgroundHook({ className, participantUserId }: any) {
|
||||||
if (className.includes("tile_")) {
|
if (className.includes("tile_")) {
|
||||||
if (data[participantUserId]) {
|
if (this.userHasBackground(participantUserId)) {
|
||||||
return {
|
return {
|
||||||
backgroundImage: `url(${data[participantUserId]})`,
|
backgroundImage: `url(${this.getImageUrl(participantUserId)})`,
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
backgroundPosition: "center",
|
backgroundPosition: "center",
|
||||||
backgroundRepeat: "no-repeat"
|
backgroundRepeat: "no-repeat"
|
||||||
|
@ -104,24 +108,35 @@ export default definePlugin({
|
||||||
|
|
||||||
useBannerHook({ displayProfile, user }: any) {
|
useBannerHook({ displayProfile, user }: any) {
|
||||||
if (displayProfile?.banner && settings.store.nitroFirst) return;
|
if (displayProfile?.banner && settings.store.nitroFirst) return;
|
||||||
if (data[user.id]) return data[user.id];
|
if (this.userHasBackground(user.id)) return this.getImageUrl(user.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
premiumHook({ userId }: any) {
|
premiumHook({ userId }: any) {
|
||||||
if (data[userId]) return 2;
|
if (this.userHasBackground(userId)) return 2;
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldShowBadge({ displayProfile, user }: any) {
|
shouldShowBadge({ displayProfile, user }: any) {
|
||||||
return displayProfile?.banner && (!data[user.id] || settings.store.nitroFirst);
|
return displayProfile?.banner && (!this.userHasBackground(user.id) || settings.store.nitroFirst);
|
||||||
|
},
|
||||||
|
|
||||||
|
userHasBackground(userId: string) {
|
||||||
|
return !!this.data?.users[userId];
|
||||||
|
},
|
||||||
|
|
||||||
|
getImageUrl(userId: string): string|null {
|
||||||
|
if (!this.userHasBackground(userId)) return null;
|
||||||
|
|
||||||
|
// We can assert that data exists because userHasBackground returned true
|
||||||
|
const { endpoint, bucket, prefix, users: { [userId]: etag } } = this.data!;
|
||||||
|
return `${endpoint}/${bucket}/${prefix}${userId}?${etag}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
enableStyle(style);
|
enableStyle(style);
|
||||||
|
|
||||||
const res = await fetch(BASE_URL);
|
const res = await fetch(API_URL);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
data = await res.json();
|
this.data = await res.json();
|
||||||
this.data = data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
7
src/plugins/validReply/README.md
Normal file
7
src/plugins/validReply/README.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# ValidReply
|
||||||
|
|
||||||
|
Fixes referenced (replied to) messages showing as "Message could not be loaded".
|
||||||
|
|
||||||
|
Hover the text to load the message!
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/45801973/d3286acf-e822-4b7f-a4e7-8ced18f581af)
|
106
src/plugins/validReply/index.ts
Normal file
106
src/plugins/validReply/index.ts
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* 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";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { FluxDispatcher, RestAPI } from "@webpack/common";
|
||||||
|
import { Message, User } from "discord-types/general";
|
||||||
|
import { Channel } from "discord-types/general/index.js";
|
||||||
|
|
||||||
|
const enum ReferencedMessageState {
|
||||||
|
Loaded,
|
||||||
|
NotLoaded,
|
||||||
|
Deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Reply {
|
||||||
|
baseAuthor: User,
|
||||||
|
baseMessage: Message;
|
||||||
|
channel: Channel;
|
||||||
|
referencedMessage: { state: ReferencedMessageState; };
|
||||||
|
compact: boolean;
|
||||||
|
isReplyAuthorBlocked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetching = new Map<string, string>();
|
||||||
|
let ReplyStore: any;
|
||||||
|
|
||||||
|
const { createMessageRecord } = findByPropsLazy("createMessageRecord");
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ValidReply",
|
||||||
|
description: 'Fixes "Message could not be loaded" upon hovering over the reply',
|
||||||
|
authors: [Devs.newwares],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "Messages.REPLY_QUOTE_MESSAGE_NOT_LOADED",
|
||||||
|
replacement: {
|
||||||
|
match: /Messages\.REPLY_QUOTE_MESSAGE_NOT_LOADED/,
|
||||||
|
replace: "$&,onMouseEnter:()=>$self.fetchReply(arguments[0])"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "ReferencedMessageStore",
|
||||||
|
replacement: {
|
||||||
|
match: /constructor\(\)\{\i\(this,"_channelCaches",new Map\)/,
|
||||||
|
replace: "$&;$self.setReplyStore(this);"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
setReplyStore(store: any) {
|
||||||
|
ReplyStore = store;
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchReply(reply: Reply) {
|
||||||
|
const { channel_id: channelId, message_id: messageId } = reply.baseMessage.messageReference!;
|
||||||
|
|
||||||
|
if (fetching.has(messageId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetching.set(messageId, channelId);
|
||||||
|
|
||||||
|
RestAPI.get({
|
||||||
|
url: `/channels/${channelId}/messages`,
|
||||||
|
query: {
|
||||||
|
limit: 1,
|
||||||
|
around: messageId
|
||||||
|
},
|
||||||
|
retries: 2
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
const reply: Message | undefined = res?.body?.[0];
|
||||||
|
if (!reply) return;
|
||||||
|
|
||||||
|
if (reply.id !== messageId) {
|
||||||
|
ReplyStore.set(channelId, messageId, {
|
||||||
|
state: ReferencedMessageState.Deleted
|
||||||
|
});
|
||||||
|
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "MESSAGE_DELETE",
|
||||||
|
channelId: channelId,
|
||||||
|
message: messageId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ReplyStore.set(reply.channel_id, reply.id, {
|
||||||
|
state: ReferencedMessageState.Loaded,
|
||||||
|
message: createMessageRecord(reply)
|
||||||
|
});
|
||||||
|
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "MESSAGE_UPDATE",
|
||||||
|
message: reply
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => { })
|
||||||
|
.finally(() => {
|
||||||
|
fetching.delete(messageId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -18,15 +18,42 @@
|
||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
import { isNonNullish } from "@utils/guards";
|
||||||
import { sleep } from "@utils/misc";
|
import { sleep } from "@utils/misc";
|
||||||
import { Queue } from "@utils/Queue";
|
import { Queue } from "@utils/Queue";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { UserStore, UserUtils, useState } from "@webpack/common";
|
import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common";
|
||||||
import type { ComponentType, ReactNode } from "react";
|
import { type ComponentType, type ReactNode } from "react";
|
||||||
|
|
||||||
|
// LYING to the type checker here
|
||||||
|
const UserFlags = Constants.UserFlags as Record<string, number>;
|
||||||
|
const badges: Record<string, ProfileBadge> = {
|
||||||
|
active_developer: { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" },
|
||||||
|
bug_hunter_level_1: { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
|
||||||
|
bug_hunter_level_2: { id: "bug_hunter_level_2", description: "Discord Bug Hunter", icon: "848f79194d4be5ff5f81505cbd0ce1e6", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
|
||||||
|
certified_moderator: { id: "certified_moderator", description: "Moderator Programs Alumni", icon: "fee1624003e2fee35cb398e125dc479b", link: "https://discord.com/safety" },
|
||||||
|
discord_employee: { id: "staff", description: "Discord Staff", icon: "5e74e9b61934fc1f67c65515d1f7e60d", link: "https://discord.com/company" },
|
||||||
|
get staff() { return this.discord_employee; },
|
||||||
|
hypesquad: { id: "hypesquad", description: "HypeSquad Events", icon: "bf01d1073931f921909045f3a39fd264", link: "https://discord.com/hypesquad" },
|
||||||
|
hypesquad_online_house_1: { id: "hypesquad_house_1", description: "HypeSquad Bravery", icon: "8a88d63823d8a71cd5e390baa45efa02", link: "https://discord.com/settings/hypesquad-online" },
|
||||||
|
hypesquad_online_house_2: { id: "hypesquad_house_2", description: "HypeSquad Brilliance", icon: "011940fd013da3f7fb926e4a1cd2e618", link: "https://discord.com/settings/hypesquad-online" },
|
||||||
|
hypesquad_online_house_3: { id: "hypesquad_house_3", description: "HypeSquad Balance", icon: "3aa41de486fa12454c3761e8e223442e", link: "https://discord.com/settings/hypesquad-online" },
|
||||||
|
partner: { id: "partner", description: "Partnered Server Owner", icon: "3f9748e53446a137a052f3454e2de41e", link: "https://discord.com/partners" },
|
||||||
|
premium: { id: "premium", description: "Subscriber", icon: "2ba85e8026a8614b640c2837bcdfe21b", link: "https://discord.com/settings/premium" },
|
||||||
|
premium_early_supporter: { id: "early_supporter", description: "Early Supporter", icon: "7060786766c9c840eb3019e725d2b358", link: "https://discord.com/settings/premium" },
|
||||||
|
verified_developer: { id: "verified_developer", description: "Early Verified Bot Developer", icon: "6df5892e0f35b051f8b61eace34f4967" },
|
||||||
|
};
|
||||||
|
|
||||||
const fetching = new Set<string>();
|
const fetching = new Set<string>();
|
||||||
const queue = new Queue(5);
|
const queue = new Queue(5);
|
||||||
|
|
||||||
|
interface ProfileBadge {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
link?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface MentionProps {
|
interface MentionProps {
|
||||||
data: {
|
data: {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
@ -43,6 +70,46 @@ interface MentionProps {
|
||||||
UserMention: ComponentType<any>;
|
UserMention: ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getUser(id: string) {
|
||||||
|
let userObj = UserStore.getUser(id);
|
||||||
|
if (userObj)
|
||||||
|
return userObj;
|
||||||
|
|
||||||
|
const user: any = await RestAPI.get({ url: Constants.Endpoints.USER(id) }).then(response => {
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "USER_UPDATE",
|
||||||
|
user: response.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.body;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Populate the profile
|
||||||
|
await FluxDispatcher.dispatch(
|
||||||
|
{
|
||||||
|
type: "USER_PROFILE_FETCH_FAILURE",
|
||||||
|
userId: id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
userObj = UserStore.getUser(id);
|
||||||
|
const fakeBadges: ProfileBadge[] = Object.entries(UserFlags)
|
||||||
|
.filter(([_, flag]) => !isNaN(flag) && userObj.hasFlag(flag))
|
||||||
|
.map(([key]) => badges[key.toLowerCase()])
|
||||||
|
.filter(isNonNullish);
|
||||||
|
if (user.premium_type || !user.bot && (user.banner || user.avatar?.startsWith?.("a_")))
|
||||||
|
fakeBadges.push(badges.premium);
|
||||||
|
|
||||||
|
// Fill in what we can deduce
|
||||||
|
const profile = UserProfileStore.getUserProfile(id);
|
||||||
|
profile.accentColor = user.accent_color;
|
||||||
|
profile.badges = fakeBadges;
|
||||||
|
profile.banner = user.banner;
|
||||||
|
profile.premiumType = user.premium_type;
|
||||||
|
|
||||||
|
return userObj;
|
||||||
|
}
|
||||||
|
|
||||||
function MentionWrapper({ data, UserMention, RoleMention, parse, props }: MentionProps) {
|
function MentionWrapper({ data, UserMention, RoleMention, parse, props }: MentionProps) {
|
||||||
const [userId, setUserId] = useState(data.userId);
|
const [userId, setUserId] = useState(data.userId);
|
||||||
|
|
||||||
|
@ -85,14 +152,14 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio
|
||||||
fetching.add(id);
|
fetching.add(id);
|
||||||
|
|
||||||
queue.unshift(() =>
|
queue.unshift(() =>
|
||||||
UserUtils.getUser(id)
|
getUser(id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setUserId(id);
|
setUserId(id);
|
||||||
fetching.delete(id);
|
fetching.delete(id);
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
if (e?.status === 429) {
|
if (e?.status === 429) {
|
||||||
queue.unshift(() => sleep(1000).then(fetch));
|
queue.unshift(() => sleep(e?.body?.retry_after ?? 1000).then(fetch));
|
||||||
fetching.delete(id);
|
fetching.delete(id);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -112,7 +179,7 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ValidUser",
|
name: "ValidUser",
|
||||||
description: "Fix mentions for unknown users showing up as '@unknown-user' (hover over a mention to fix it)",
|
description: "Fix mentions for unknown users showing up as '@unknown-user' (hover over a mention to fix it)",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven, Devs.Dolfies],
|
||||||
tags: ["MentionCacheFix"],
|
tags: ["MentionCacheFix"],
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
|
@ -138,6 +205,7 @@ export default definePlugin({
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary noop>
|
<ErrorBoundary noop>
|
||||||
<MentionWrapper
|
<MentionWrapper
|
||||||
|
key={"mention" + data.userId}
|
||||||
RoleMention={RoleMention}
|
RoleMention={RoleMention}
|
||||||
UserMention={UserMention}
|
UserMention={UserMention}
|
||||||
data={data}
|
data={data}
|
||||||
|
|
55
src/plugins/voiceDownload/index.tsx
Normal file
55
src/plugins/voiceDownload/index.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "VoiceDownload",
|
||||||
|
description: "Adds a download to voice messages. (Opens a new browser tab)",
|
||||||
|
authors: [Devs.puv],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "rippleContainer,children",
|
||||||
|
replacement: {
|
||||||
|
match: /\(0,\i\.jsx\).{0,150},children:.{0,50}\("source",{src:(\i)}\)}\)/,
|
||||||
|
replace: "[$&, $self.renderDownload($1)]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
renderDownload(src: string) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className="vc-voice-download"
|
||||||
|
href={src}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
aria-label="Download voice message"
|
||||||
|
{...IS_DISCORD_DESKTOP
|
||||||
|
? { target: "_blank" } // open externally
|
||||||
|
: { download: "voice-message.ogg" } // download directly (not supported on discord desktop)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<this.Icon />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
Icon: () => (
|
||||||
|
<svg
|
||||||
|
height="24"
|
||||||
|
width="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 2a1 1 0 0 1 1 1v10.59l3.3-3.3a1 1 0 1 1 1.4 1.42l-5 5a1 1 0 0 1-1.4 0l-5-5a1 1 0 1 1 1.4-1.42l3.3 3.3V3a1 1 0 0 1 1-1ZM3 20a1 1 0 1 0 0 2h18a1 1 0 1 0 0-2H3Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
});
|
12
src/plugins/voiceDownload/style.css
Normal file
12
src/plugins/voiceDownload/style.css
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.vc-voice-download {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
color: var(--interactive-normal);
|
||||||
|
margin-left: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-voice-download:hover {
|
||||||
|
color: var(--interactive-active);
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ import { useAwaiter } from "@utils/react";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { chooseFile } from "@utils/web";
|
import { chooseFile } from "@utils/web";
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
import { Button, Card, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
|
import { Button, Card, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
|
||||||
import { ComponentType } from "react";
|
import { ComponentType } from "react";
|
||||||
|
|
||||||
import { VoiceRecorderDesktop } from "./DesktopRecorder";
|
import { VoiceRecorderDesktop } from "./DesktopRecorder";
|
||||||
|
@ -98,7 +98,7 @@ function sendAudio(blob: Blob, meta: AudioMetadata) {
|
||||||
|
|
||||||
upload.on("complete", () => {
|
upload.on("complete", () => {
|
||||||
RestAPI.post({
|
RestAPI.post({
|
||||||
url: `/channels/${channelId}/messages`,
|
url: Constants.Endpoints.MESSAGES(channelId),
|
||||||
body: {
|
body: {
|
||||||
flags: 1 << 13,
|
flags: 1 << 13,
|
||||||
channel_id: channelId,
|
channel_id: channelId,
|
||||||
|
|
30
src/plugins/webScreenShareFixes.web/index.ts
Normal file
30
src/plugins/webScreenShareFixes.web/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* 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: "WebScreenShareFixes",
|
||||||
|
authors: [Devs.Kaitlyn],
|
||||||
|
description: "Removes 2500kbps bitrate cap on chromium and vesktop clients.",
|
||||||
|
enabledByDefault: true,
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "x-google-max-bitrate",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /"x-google-max-bitrate=".concat\(\i\)/,
|
||||||
|
replace: '"x-google-max-bitrate=".concat("80_000")'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /;level-asymmetry-allowed=1/,
|
||||||
|
replace: ";b=AS:800000;level-asymmetry-allowed=1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
|
@ -23,7 +23,7 @@ import { Queue } from "@utils/Queue";
|
||||||
import { useForceUpdater } from "@utils/react";
|
import { useForceUpdater } from "@utils/react";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||||
import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
|
import { ChannelStore, Constants, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
|
||||||
import { CustomEmoji } from "@webpack/types";
|
import { CustomEmoji } from "@webpack/types";
|
||||||
import { Message, ReactionEmoji, User } from "discord-types/general";
|
import { Message, ReactionEmoji, User } from "discord-types/general";
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ let reactions: Record<string, ReactionCacheEntry>;
|
||||||
function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
|
function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
|
||||||
const key = emoji.name + (emoji.id ? `:${emoji.id}` : "");
|
const key = emoji.name + (emoji.id ? `:${emoji.id}` : "");
|
||||||
return RestAPI.get({
|
return RestAPI.get({
|
||||||
url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${key}`,
|
url: Constants.Endpoints.REACTIONS(msg.channel_id, msg.id, key),
|
||||||
query: {
|
query: {
|
||||||
limit: 100,
|
limit: 100,
|
||||||
type
|
type
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Vencord, a Discord client mod
|
* Vencord, a Discord client mod
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -13,10 +13,7 @@ import { findByPropsLazy } from "@webpack";
|
||||||
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
|
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
|
||||||
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
|
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
|
||||||
|
|
||||||
const enum ChannelTypes {
|
const { ChannelTypes } = findByPropsLazy("ChannelTypes");
|
||||||
DM = 1,
|
|
||||||
GROUP_DM = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
guild_id: string,
|
guild_id: string,
|
||||||
|
@ -71,15 +68,35 @@ interface Call {
|
||||||
ringing: string[];
|
ringing: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MuteStore = findByPropsLazy("isSuppressEveryoneEnabled");
|
const Notifs = findByPropsLazy("makeTextChatNotification");
|
||||||
const XSLog = new Logger("XSOverlay");
|
const XSLog = new Logger("XSOverlay");
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
ignoreBots: {
|
botNotifications: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Ignore messages from bots",
|
description: "Allow bot notifications",
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
serverNotifications: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Allow server notifications",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
dmNotifications: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Allow Direct Message notifications",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
groupDmNotifications: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Allow Group DM notifications",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
callNotifications: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Allow call notifications",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
pingColor: {
|
pingColor: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "User mention color",
|
description: "User mention color",
|
||||||
|
@ -97,8 +114,13 @@ const settings = definePluginSettings({
|
||||||
},
|
},
|
||||||
timeout: {
|
timeout: {
|
||||||
type: OptionType.NUMBER,
|
type: OptionType.NUMBER,
|
||||||
description: "Notif duration (secs)",
|
description: "Notification duration (secs)",
|
||||||
default: 1.0,
|
default: 3,
|
||||||
|
},
|
||||||
|
lengthBasedTimeout: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Extend duration with message length",
|
||||||
|
default: true
|
||||||
},
|
},
|
||||||
opacity: {
|
opacity: {
|
||||||
type: OptionType.SLIDER,
|
type: OptionType.SLIDER,
|
||||||
|
@ -124,7 +146,7 @@ export default definePlugin({
|
||||||
settings,
|
settings,
|
||||||
flux: {
|
flux: {
|
||||||
CALL_UPDATE({ call }: { call: Call; }) {
|
CALL_UPDATE({ call }: { call: Call; }) {
|
||||||
if (call?.ringing?.includes(UserStore.getCurrentUser().id)) {
|
if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) {
|
||||||
const channel = ChannelStore.getChannel(call.channel_id);
|
const channel = ChannelStore.getChannel(call.channel_id);
|
||||||
sendOtherNotif("Incoming call", `${channel.name} is calling you...`);
|
sendOtherNotif("Incoming call", `${channel.name} is calling you...`);
|
||||||
}
|
}
|
||||||
|
@ -134,7 +156,7 @@ export default definePlugin({
|
||||||
try {
|
try {
|
||||||
if (optimistic) return;
|
if (optimistic) return;
|
||||||
const channel = ChannelStore.getChannel(message.channel_id);
|
const channel = ChannelStore.getChannel(message.channel_id);
|
||||||
if (!shouldNotify(message, channel)) return;
|
if (!shouldNotify(message, message.channel_id)) return;
|
||||||
|
|
||||||
const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
|
const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
|
||||||
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
|
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
|
||||||
|
@ -194,6 +216,7 @@ export default definePlugin({
|
||||||
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`);
|
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// color role mentions (unity styling btw lol)
|
||||||
if (message.mention_roles.length > 0) {
|
if (message.mention_roles.length > 0) {
|
||||||
for (const roleId of message.mention_roles) {
|
for (const roleId of message.mention_roles) {
|
||||||
const role = GuildStore.getRole(channel.guild_id, roleId);
|
const role = GuildStore.getRole(channel.guild_id, roleId);
|
||||||
|
@ -213,6 +236,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// color channel mentions
|
||||||
if (channelMatches) {
|
if (channelMatches) {
|
||||||
for (const cMatch of channelMatches) {
|
for (const cMatch of channelMatches) {
|
||||||
let channelId = cMatch.split("<#")[1];
|
let channelId = cMatch.split("<#")[1];
|
||||||
|
@ -221,6 +245,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldIgnoreForChannelType(channel)) return;
|
||||||
sendMsgNotif(titleString, finalMsg, message);
|
sendMsgNotif(titleString, finalMsg, message);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
|
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
|
||||||
|
@ -229,13 +254,19 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function shouldIgnoreForChannelType(channel: Channel) {
|
||||||
|
if (channel.type === ChannelTypes.DM && settings.store.dmNotifications) return false;
|
||||||
|
if (channel.type === ChannelTypes.GROUP_DM && settings.store.groupDmNotifications) return false;
|
||||||
|
else return !settings.store.serverNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
function sendMsgNotif(titleString: string, content: string, message: Message) {
|
function sendMsgNotif(titleString: string, content: string, message: Message) {
|
||||||
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
|
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
|
||||||
const msgData = {
|
const msgData = {
|
||||||
messageType: 1,
|
messageType: 1,
|
||||||
index: 0,
|
index: 0,
|
||||||
timeout: settings.store.timeout,
|
timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
|
||||||
height: calculateHeight(cleanMessage(content)),
|
height: calculateHeight(content),
|
||||||
opacity: settings.store.opacity,
|
opacity: settings.store.opacity,
|
||||||
volume: settings.store.volume,
|
volume: settings.store.volume,
|
||||||
audioPath: settings.store.soundPath,
|
audioPath: settings.store.soundPath,
|
||||||
|
@ -253,8 +284,8 @@ function sendOtherNotif(content: string, titleString: string) {
|
||||||
const msgData = {
|
const msgData = {
|
||||||
messageType: 1,
|
messageType: 1,
|
||||||
index: 0,
|
index: 0,
|
||||||
timeout: settings.store.timeout,
|
timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
|
||||||
height: calculateHeight(cleanMessage(content)),
|
height: calculateHeight(content),
|
||||||
opacity: settings.store.opacity,
|
opacity: settings.store.opacity,
|
||||||
volume: settings.store.volume,
|
volume: settings.store.volume,
|
||||||
audioPath: settings.store.soundPath,
|
audioPath: settings.store.soundPath,
|
||||||
|
@ -267,13 +298,11 @@ function sendOtherNotif(content: string, titleString: string) {
|
||||||
Native.sendToOverlay(msgData);
|
Native.sendToOverlay(msgData);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldNotify(message: Message, channel: Channel) {
|
function shouldNotify(message: Message, channel: string) {
|
||||||
const currentUser = UserStore.getCurrentUser();
|
const currentUser = UserStore.getCurrentUser();
|
||||||
if (message.author.id === currentUser.id) return false;
|
if (message.author.id === currentUser.id) return false;
|
||||||
if (message.author.bot && settings.store.ignoreBots) return false;
|
if (message.author.bot && !settings.store.botNotifications) return false;
|
||||||
if (MuteStore.allowAllMessages(channel) || message.mention_everyone && !MuteStore.isSuppressEveryoneEnabled(message.guild_id)) return true;
|
return Notifs.shouldNotify(message, channel);
|
||||||
|
|
||||||
return message.mentions.some(m => m.id === currentUser.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateHeight(content: string) {
|
function calculateHeight(content: string) {
|
||||||
|
@ -283,6 +312,9 @@ function calculateHeight(content: string) {
|
||||||
return 250;
|
return 250;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanMessage(content: string) {
|
function calculateTimeout(content: string) {
|
||||||
return content.replace(new RegExp("<[^>]*>", "g"), "");
|
if (content.length <= 100) return 3;
|
||||||
|
if (content.length <= 200) return 4;
|
||||||
|
if (content.length <= 300) return 5;
|
||||||
|
return 6;
|
||||||
}
|
}
|
||||||
|
|
|
@ -266,6 +266,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "Dziurwa",
|
name: "Dziurwa",
|
||||||
id: 1001086404203389018n
|
id: 1001086404203389018n
|
||||||
},
|
},
|
||||||
|
arHSM: {
|
||||||
|
name: "arHSM",
|
||||||
|
id: 841509053422632990n
|
||||||
|
},
|
||||||
F53: {
|
F53: {
|
||||||
name: "F53",
|
name: "F53",
|
||||||
id: 280411966126948353n
|
id: 280411966126948353n
|
||||||
|
@ -374,10 +378,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "ProffDea",
|
name: "ProffDea",
|
||||||
id: 609329952180928513n
|
id: 609329952180928513n
|
||||||
},
|
},
|
||||||
|
UlyssesZhan: {
|
||||||
|
name: "UlyssesZhan",
|
||||||
|
id: 586808226058862623n
|
||||||
|
},
|
||||||
ant0n: {
|
ant0n: {
|
||||||
name: "ant0n",
|
name: "ant0n",
|
||||||
id: 145224646868860928n
|
id: 145224646868860928n
|
||||||
},
|
},
|
||||||
|
Board: {
|
||||||
|
name: "BoardTM",
|
||||||
|
id: 285475344817848320n,
|
||||||
|
},
|
||||||
philipbry: {
|
philipbry: {
|
||||||
name: "philipbry",
|
name: "philipbry",
|
||||||
id: 554994003318276106n
|
id: 554994003318276106n
|
||||||
|
@ -414,6 +426,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "Kyuuhachi",
|
name: "Kyuuhachi",
|
||||||
id: 236588665420251137n,
|
id: 236588665420251137n,
|
||||||
},
|
},
|
||||||
|
nin0dev: {
|
||||||
|
name: "nin0dev",
|
||||||
|
id: 886685857560539176n
|
||||||
|
},
|
||||||
Elvyra: {
|
Elvyra: {
|
||||||
name: "Elvyra",
|
name: "Elvyra",
|
||||||
id: 708275751816003615n,
|
id: 708275751816003615n,
|
||||||
|
@ -426,6 +442,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "newwares",
|
name: "newwares",
|
||||||
id: 421405303951851520n
|
id: 421405303951851520n
|
||||||
},
|
},
|
||||||
|
puv: {
|
||||||
|
name: "puv",
|
||||||
|
id: 469441552251355137n
|
||||||
|
},
|
||||||
Kodarru: {
|
Kodarru: {
|
||||||
name: "Kodarru",
|
name: "Kodarru",
|
||||||
id: 785227396218748949n
|
id: 785227396218748949n
|
||||||
|
@ -453,7 +473,27 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
nickwoah: {
|
nickwoah: {
|
||||||
name: "nickwoah",
|
name: "nickwoah",
|
||||||
id: 644298972420374528n
|
id: 644298972420374528n
|
||||||
}
|
},
|
||||||
|
HAHALOSAH: {
|
||||||
|
name: "HAHALOSAH",
|
||||||
|
id: 903418691268513883n
|
||||||
|
},
|
||||||
|
GabiRP: {
|
||||||
|
name: "GabiRP",
|
||||||
|
id: 507955112027750401n
|
||||||
|
},
|
||||||
|
ImBanana: {
|
||||||
|
name: "Im_Banana",
|
||||||
|
id: 635250116688871425n
|
||||||
|
},
|
||||||
|
xocherry: {
|
||||||
|
name: "xocherry",
|
||||||
|
id: 221288171013406720n
|
||||||
|
},
|
||||||
|
ScattrdBlade: {
|
||||||
|
name: "ScattrdBlade",
|
||||||
|
id: 678007540608532491n
|
||||||
|
},
|
||||||
} satisfies Record<string, Dev>);
|
} satisfies Record<string, Dev>);
|
||||||
|
|
||||||
// iife so #__PURE__ works correctly
|
// iife so #__PURE__ works correctly
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue