mirror of
https://github.com/Vendicated/Vencord.git
synced 2025-02-24 15:35:11 +00:00
Merge branch 'Vendicated:main' into main
This commit is contained in:
commit
8eab8c1cc6
146 changed files with 6379 additions and 3050 deletions
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
|
@ -18,14 +18,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
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
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 19
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- 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'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- 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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: check that tag matches package.json version
|
||||
run: |
|
||||
|
@ -20,12 +20,12 @@ jobs:
|
|||
exit 1
|
||||
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
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 19
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
23
.github/workflows/reportBrokenPlugins.yml
vendored
23
.github/workflows/reportBrokenPlugins.yml
vendored
|
@ -11,28 +11,31 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ github.event_name == 'schedule' }}
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
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
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 19
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
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
|
||||
run: pnpm buildWeb --standalone --dev
|
||||
|
@ -41,7 +44,7 @@ jobs:
|
|||
timeout-minutes: 10
|
||||
run: |
|
||||
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
|
||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||
|
@ -54,7 +57,7 @@ jobs:
|
|||
if: success() || failure() # even run if previous one failed
|
||||
run: |
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
@ -1,11 +1,9 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"EditorConfig.EditorConfig",
|
||||
"ExodiusStudios.comment-anchors",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"GregorBiswanger.json2ts",
|
||||
"stylelint.vscode-stylelint"
|
||||
"stylelint.vscode-stylelint",
|
||||
"Vendicated.vencord-companion"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.7.4",
|
||||
"version": "1.8.4",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
|
@ -71,7 +71,7 @@
|
|||
"zip-local": "^0.3.5",
|
||||
"zustand": "^3.7.2"
|
||||
},
|
||||
"packageManager": "pnpm@8.10.2",
|
||||
"packageManager": "pnpm@9.1.0",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
||||
|
|
4213
pnpm-lock.yaml
generated
4213
pnpm-lock.yaml
generated
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 { promisify } from "util";
|
||||
|
||||
// wtf is this assert syntax
|
||||
import PackageJSON from "../../package.json" assert { type: "json" };
|
||||
import { getPluginTarget } from "../utils.mjs";
|
||||
|
||||
/** @type {import("../../package.json")} */
|
||||
const PackageJSON = JSON.parse(readFileSync("package.json"));
|
||||
|
||||
export const VERSION = PackageJSON.version;
|
||||
// https://reproducible-builds.org/docs/source-date-epoch/
|
||||
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);
|
||||
|
||||
function runTime(token: string) {
|
||||
async function runtime(token: string) {
|
||||
console.log("[PUP_DEBUG]", "Starting test...");
|
||||
|
||||
try {
|
||||
|
@ -282,9 +282,13 @@ function runTime(token: string) {
|
|||
|
||||
// Monkey patch Logger to not log with custom css
|
||||
// @ts-ignore
|
||||
const originalLog = Vencord.Util.Logger.prototype._log;
|
||||
// @ts-ignore
|
||||
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
|
||||
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
|
||||
|
@ -310,45 +314,30 @@ function runTime(token: string) {
|
|||
});
|
||||
});
|
||||
|
||||
Vencord.Webpack.waitFor(
|
||||
"loginToken",
|
||||
m => {
|
||||
console.log("[PUP_DEBUG]", "Logging in with token...");
|
||||
m.loginToken(token);
|
||||
}
|
||||
);
|
||||
let wreq: typeof Vencord.Webpack.wreq;
|
||||
|
||||
// Force load all chunks
|
||||
Vencord.Webpack.onceReady.then(() => setTimeout(async () => {
|
||||
console.log("[PUP_DEBUG]", "Webpack is ready!");
|
||||
const { canonicalizeMatch, Logger } = Vencord.Util;
|
||||
|
||||
const { wreq } = Vencord.Webpack;
|
||||
|
||||
console.log("[PUP_DEBUG]", "Loading all chunks...");
|
||||
|
||||
let chunks = null as Record<number, string[]> | null;
|
||||
const sym = Symbol("Vencord.chunksExtract");
|
||||
|
||||
Object.defineProperty(Object.prototype, sym, {
|
||||
get() {
|
||||
chunks = this;
|
||||
},
|
||||
set() { },
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
await (wreq as any).el(sym);
|
||||
delete Object.prototype[sym];
|
||||
|
||||
const validChunksEntryPoints = new Set<string>();
|
||||
const validChunks = new Set<string>();
|
||||
const invalidChunks = new Set<string>();
|
||||
|
||||
if (!chunks) throw new Error("Failed to get chunks");
|
||||
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
||||
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
||||
|
||||
for (const entryPoint in chunks) {
|
||||
const chunkIds = chunks[entryPoint];
|
||||
let invalidEntryPoint = false;
|
||||
// True if resolved, false otherwise
|
||||
const chunksSearchPromises = [] as Array<() => boolean>;
|
||||
const lazyChunkRegex = canonicalizeMatch(/Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/g);
|
||||
const chunkIdsRegex = canonicalizeMatch(/\("(.+?)"\)/g);
|
||||
|
||||
async function searchAndLoadLazyChunks(factoryCode: string) {
|
||||
const lazyChunks = factoryCode.matchAll(lazyChunkRegex);
|
||||
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
|
||||
|
||||
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
||||
const chunkIds = Array.from(rawChunkIds.matchAll(chunkIdsRegex)).map(m => m[1]);
|
||||
if (chunkIds.length === 0) return;
|
||||
|
||||
let invalidChunkGroup = false;
|
||||
|
||||
for (const id of chunkIds) {
|
||||
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
||||
|
@ -359,56 +348,28 @@ function runTime(token: string) {
|
|||
|
||||
if (isWasm) {
|
||||
invalidChunks.add(id);
|
||||
invalidEntryPoint = true;
|
||||
invalidChunkGroup = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
validChunks.add(id);
|
||||
}
|
||||
|
||||
if (!invalidEntryPoint)
|
||||
validChunksEntryPoints.add(entryPoint);
|
||||
if (!invalidChunkGroup) {
|
||||
validChunkGroups.add([chunkIds, entryPoint]);
|
||||
}
|
||||
}));
|
||||
|
||||
for (const entryPoint of validChunksEntryPoints) {
|
||||
try {
|
||||
// Loads all chunks required for an entry point
|
||||
await (wreq as any).el(entryPoint);
|
||||
} catch (err) { }
|
||||
}
|
||||
// Loads all found valid chunk groups
|
||||
await Promise.all(
|
||||
Array.from(validChunkGroups)
|
||||
.map(([chunkIds]) =>
|
||||
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
||||
)
|
||||
);
|
||||
|
||||
// Matches "id" or id:
|
||||
const chunkIdRegex = /(?:"(\d+?)")|(?:(\d+?):)/g;
|
||||
const wreqU = wreq.u.toString();
|
||||
|
||||
const allChunks = [] as string[];
|
||||
let currentMatch: RegExpExecArray | null;
|
||||
|
||||
while ((currentMatch = chunkIdRegex.exec(wreqU)) != null) {
|
||||
const id = currentMatch[1] ?? currentMatch[2];
|
||||
if (id == null) continue;
|
||||
|
||||
allChunks.push(id);
|
||||
}
|
||||
|
||||
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
||||
const chunksLeft = allChunks.filter(id => {
|
||||
return !(validChunks.has(id) || invalidChunks.has(id));
|
||||
});
|
||||
|
||||
for (const id of chunksLeft) {
|
||||
const isWasm = await fetch(wreq.p + wreq.u(id))
|
||||
.then(r => r.text())
|
||||
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
||||
|
||||
// Loads a chunk
|
||||
if (!isWasm) await wreq.e(id as any);
|
||||
}
|
||||
|
||||
// Make sure every chunk has finished loading
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
for (const entryPoint of validChunksEntryPoints) {
|
||||
// Requires the entry points for all valid chunk groups
|
||||
for (const [, entryPoint] of validChunkGroups) {
|
||||
try {
|
||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||
} catch (err) {
|
||||
|
@ -416,11 +377,97 @@ function runTime(token: string) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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 (let i = 0; i < chunksSearchPromises.length; i++) {
|
||||
const isResolved = chunksSearchPromises[i]();
|
||||
|
||||
if (isResolved) {
|
||||
// Remove finished promises to avoid having to iterate through a huge array everytime
|
||||
chunksSearchPromises.splice(i--, 1);
|
||||
} else {
|
||||
allResolved = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (allResolved) chunksSearchingResolve();
|
||||
}, 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 Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
||||
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -445,7 +492,7 @@ function runTime(token: string) {
|
|||
const [code, matcher] = args;
|
||||
|
||||
const module = Vencord.Webpack.findModuleFactory(...code);
|
||||
if (module) result = module.toString().match(Vencord.Util.canonicalizeMatch(matcher));
|
||||
if (module) result = module.toString().match(canonicalizeMatch(matcher));
|
||||
} else {
|
||||
// @ts-ignore
|
||||
result = Vencord.Webpack[method](...args);
|
||||
|
@ -463,7 +510,6 @@ function runTime(token: string) {
|
|||
}
|
||||
|
||||
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
|
||||
}, 1000));
|
||||
} catch (e) {
|
||||
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
|
||||
process.exit(1);
|
||||
|
@ -473,7 +519,7 @@ function runTime(token: string) {
|
|||
await page.evaluateOnNewDocument(`
|
||||
${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");
|
||||
|
|
|
@ -27,6 +27,7 @@ export { PlainSettings, Settings };
|
|||
import "./utils/quickCss";
|
||||
import "./webpack/patchWebpack";
|
||||
|
||||
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
||||
import { StartAt } from "@utils/types";
|
||||
|
||||
import { get as dsGet } from "./api/DataStore";
|
||||
|
@ -85,7 +86,7 @@ async function init() {
|
|||
|
||||
syncSettings();
|
||||
|
||||
if (!IS_WEB) {
|
||||
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
||||
try {
|
||||
const isOutdated = await checkForUpdates();
|
||||
if (!isOutdated) return;
|
||||
|
@ -103,15 +104,12 @@ async function init() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (Settings.notifyAboutUpdates)
|
||||
setTimeout(() => showNotification({
|
||||
title: "A Vencord update is available!",
|
||||
body: "Click here to view the update",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick() {
|
||||
SettingsRouter.open("VencordUpdater");
|
||||
}
|
||||
onClick: openUpdaterModal!
|
||||
}), 10_000);
|
||||
} catch (err) {
|
||||
UpdateLogger.error("Failed to check for updates", err);
|
||||
|
|
|
@ -36,7 +36,7 @@ export interface ProfileBadge {
|
|||
image?: string;
|
||||
link?: string;
|
||||
/** 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? */
|
||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||
|
@ -87,9 +87,7 @@ export function _getBadges(args: BadgeUserArgs) {
|
|||
|
||||
export interface BadgeUserArgs {
|
||||
user: User;
|
||||
profile: Profile;
|
||||
premiumSince: Date;
|
||||
premiumGuildSince?: Date;
|
||||
guildId: string;
|
||||
}
|
||||
|
||||
interface ConnectedAccount {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* 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 { MessageActions, SnowflakeUtils } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
|
|
@ -20,7 +20,7 @@ import { debounce } from "@shared/debounce";
|
|||
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
|
||||
import { localStorage } from "@utils/localStorage";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { mergeDefaults } from "@utils/misc";
|
||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||
import { putCloudSettings } from "@utils/settingsSync";
|
||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
|
@ -29,7 +29,6 @@ import plugins from "~plugins";
|
|||
|
||||
const logger = new Logger("Settings");
|
||||
export interface Settings {
|
||||
notifyAboutUpdates: boolean;
|
||||
autoUpdate: boolean;
|
||||
autoUpdateNotification: boolean,
|
||||
useQuickCss: boolean;
|
||||
|
@ -78,8 +77,7 @@ export interface Settings {
|
|||
}
|
||||
|
||||
const DefaultSettings: Settings = {
|
||||
notifyAboutUpdates: true,
|
||||
autoUpdate: false,
|
||||
autoUpdate: true,
|
||||
autoUpdateNotification: true,
|
||||
useQuickCss: true,
|
||||
themeLinks: [],
|
||||
|
|
|
@ -9,10 +9,12 @@ import "./contributorModal.css";
|
|||
import { useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Link } from "@components/Link";
|
||||
import { DevsById } from "@utils/constants";
|
||||
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
||||
import { pluralise } from "@utils/misc";
|
||||
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 Plugins from "~plugins";
|
||||
|
@ -72,6 +74,8 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
||||
}, [user.id, user.username]);
|
||||
|
||||
const ContributedHyperLink = <Link href="https://vencord.dev/source">contributed</Link>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cl("header")}>
|
||||
|
@ -84,20 +88,37 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
|
||||
<div className={cl("links")}>
|
||||
{website && (
|
||||
<MaskedLink
|
||||
href={"https://" + website}
|
||||
>
|
||||
<Tooltip text={website}>
|
||||
{props => (
|
||||
<MaskedLink {...props} href={"https://" + website}>
|
||||
<WebsiteIcon />
|
||||
</MaskedLink>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
{githubName && (
|
||||
<MaskedLink href={`https://github.com/${githubName}`}>
|
||||
<Tooltip text={githubName}>
|
||||
{props => (
|
||||
<MaskedLink {...props} href={`https://github.com/${githubName}`}>
|
||||
<GithubIcon />
|
||||
</MaskedLink>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{plugins.length ? (
|
||||
<Forms.FormText>
|
||||
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
|
||||
</Forms.FormText>
|
||||
) : (
|
||||
<Forms.FormText>
|
||||
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
|
||||
</Forms.FormText>
|
||||
)}
|
||||
|
||||
{!!plugins.length && (
|
||||
<div className={cl("plugins")}>
|
||||
{plugins.map(p =>
|
||||
<PluginCard
|
||||
|
@ -108,6 +129,7 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,11 +25,13 @@
|
|||
display: block;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 16px;
|
||||
width: 32px;
|
||||
background: var(--background-tertiary);
|
||||
z-index: -1;
|
||||
left: -16px;
|
||||
left: -32px;
|
||||
top: 0;
|
||||
border-top-left-radius: 9999px;
|
||||
border-bottom-left-radius: 9999px;
|
||||
}
|
||||
|
||||
.vc-author-modal-avatar {
|
||||
|
@ -55,4 +57,5 @@
|
|||
.vc-author-modal-plugins {
|
||||
display: grid;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import PluginModal from "@components/PluginSettings/PluginModal";
|
|||
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
||||
import { SettingsTab } from "@components/VencordSettings/shared";
|
||||
import { ChangeList } from "@utils/ChangeList";
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
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 { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
||||
|
||||
// Avoid circular dependency
|
||||
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins"));
|
||||
|
||||
const cl = classNameFactory("vc-plugins-");
|
||||
const logger = new Logger("PluginSettings", "#a6d189");
|
||||
|
@ -315,7 +316,6 @@ export default function PluginSettings() {
|
|||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
|
||||
<div className={InputStyles.inputWrapper}>
|
||||
<Select
|
||||
className={InputStyles.inputDefault}
|
||||
options={[
|
||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||
|
|
|
@ -21,7 +21,7 @@ import "./addonCard.css";
|
|||
import { classNameFactory } from "@api/Styles";
|
||||
import { Badge } from "@components/Badge";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { Text } from "@webpack/common";
|
||||
import { Text, useRef } from "@webpack/common";
|
||||
import type { MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
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) {
|
||||
const titleRef = useRef<HTMLDivElement>(null);
|
||||
const titleContainerRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<div
|
||||
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("name-author")}>
|
||||
<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>
|
||||
{!!author && (
|
||||
<Text variant="text-md/normal" className={cl("author")}>
|
||||
|
|
|
@ -180,7 +180,8 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||
|
||||
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
|
||||
value={replacement?.toString()}
|
||||
onChange={onChange}
|
||||
|
@ -188,7 +189,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||
/>
|
||||
{!isFunc && (
|
||||
<div className="vc-text-selectable">
|
||||
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
||||
<Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>
|
||||
{Object.entries({
|
||||
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
||||
"$$": "Insert a $",
|
||||
|
@ -220,11 +221,12 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||
|
||||
interface FullPatchInputProps {
|
||||
setFind(v: string): void;
|
||||
setParsedFind(v: string | RegExp): void;
|
||||
setMatch(v: string): 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 [fullPatchError, setFullPatchError] = React.useState<string>("");
|
||||
|
||||
|
@ -233,6 +235,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
|||
setFullPatchError("");
|
||||
|
||||
setFind("");
|
||||
setParsedFind("");
|
||||
setMatch("");
|
||||
setReplacement("");
|
||||
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.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);
|
||||
setReplacement(parsed.replacement.replace);
|
||||
setFullPatchError("");
|
||||
|
@ -266,7 +270,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
|||
}
|
||||
|
||||
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} />
|
||||
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
|
||||
</>;
|
||||
|
@ -274,6 +278,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
|||
|
||||
function PatchHelper() {
|
||||
const [find, setFind] = React.useState<string>("");
|
||||
const [parsedFind, setParsedFind] = React.useState<string | RegExp>("");
|
||||
const [match, setMatch] = React.useState<string>("");
|
||||
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
||||
|
||||
|
@ -285,20 +290,34 @@ function PatchHelper() {
|
|||
const code = React.useMemo(() => {
|
||||
return `
|
||||
{
|
||||
find: ${JSON.stringify(find)},
|
||||
find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
|
||||
replacement: {
|
||||
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
||||
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
||||
}
|
||||
}
|
||||
`.trim();
|
||||
}, [find, match, replacement]);
|
||||
}, [parsedFind, match, replacement]);
|
||||
|
||||
function onFindChange(v: string) {
|
||||
setFindError(void 0);
|
||||
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>
|
||||
<FullPatchInput
|
||||
setFind={onFindChange}
|
||||
setParsedFind={setParsedFind}
|
||||
setMatch={onMatchChange}
|
||||
setReplacement={setReplacement}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle>find</Forms.FormTitle>
|
||||
<Forms.FormTitle className={Margins.top8}>find</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={find}
|
||||
onChange={onFindChange}
|
||||
onBlur={onFindBlur}
|
||||
error={findError}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle>match</Forms.FormTitle>
|
||||
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
||||
<CheckedTextInput
|
||||
value={match}
|
||||
onChange={onMatchChange}
|
||||
|
@ -342,6 +363,7 @@ function PatchHelper() {
|
|||
}}
|
||||
/>
|
||||
|
||||
<div className={Margins.top8} />
|
||||
<ReplacementInput
|
||||
replacement={replacement}
|
||||
setReplacement={setReplacement}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { Flex } from "@components/Flex";
|
|||
import { Link } from "@components/Link";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
|
||||
|
@ -29,7 +30,7 @@ import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@web
|
|||
|
||||
import gitHash from "~git-hash";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
import { handleSettingsTabError, SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||
return async () => {
|
||||
|
@ -38,21 +39,24 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
|
|||
await action();
|
||||
} catch (e: any) {
|
||||
UpdateLogger.error("Failed to update", e);
|
||||
|
||||
let err: string;
|
||||
if (!e) {
|
||||
var err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
||||
err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
||||
} else if (e.code && e.cmd) {
|
||||
const { code, path, cmd, stderr } = e;
|
||||
|
||||
if (code === "ENOENT")
|
||||
var err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||
err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||
else {
|
||||
var err = `An error occurred while running \`${cmd}\`:\n`;
|
||||
err = `An error occurred while running \`${cmd}\`:\n`;
|
||||
err += stderr || `Code \`${code}\`. See the console for more info`;
|
||||
}
|
||||
|
||||
} else {
|
||||
var err = "An unknown error occurred. See the console for more info.";
|
||||
err = "An unknown error occurred. See the console for more info.";
|
||||
}
|
||||
|
||||
Alerts.show({
|
||||
title: "Oops!",
|
||||
body: (
|
||||
|
@ -186,7 +190,7 @@ function Newer(props: CommonProps) {
|
|||
}
|
||||
|
||||
function Updater() {
|
||||
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
|
||||
const settings = useSettings(["autoUpdate", "autoUpdateNotification"]);
|
||||
|
||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||
|
||||
|
@ -203,14 +207,6 @@ function Updater() {
|
|||
return (
|
||||
<SettingsTab title="Vencord Updater">
|
||||
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||
<Switch
|
||||
value={settings.notifyAboutUpdates}
|
||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||
note="Shows a notification on startup"
|
||||
disabled={settings.autoUpdate}
|
||||
>
|
||||
Get notified about new updates
|
||||
</Switch>
|
||||
<Switch
|
||||
value={settings.autoUpdate}
|
||||
onChange={(v: boolean) => settings.autoUpdate = v}
|
||||
|
@ -253,3 +249,20 @@ function Updater() {
|
|||
}
|
||||
|
||||
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
|
||||
|
||||
export const openUpdaterModal = IS_UPDATER_DISABLED ? null : function () {
|
||||
const UpdaterTab = wrapTab(Updater, "Updater");
|
||||
|
||||
try {
|
||||
openModal(wrapTab((modalProps: ModalProps) => (
|
||||
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<ModalContent className="vc-updater-modal">
|
||||
<ModalCloseButton onClick={modalProps.onClose} className="vc-updater-modal-close-button" />
|
||||
<UpdaterTab />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
), "UpdaterModal"));
|
||||
} catch {
|
||||
handleSettingsTabError();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -62,3 +62,36 @@
|
|||
.vc-addon-author::before {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -65,3 +65,11 @@
|
|||
/* discord also sets cursor: default which prevents the cursor from showing as text */
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.vc-updater-modal {
|
||||
padding: 1.5em !important;
|
||||
}
|
||||
|
||||
.vc-updater-modal-close-button {
|
||||
float: right;
|
||||
}
|
||||
|
|
|
@ -42,11 +42,11 @@ export function SettingsTab({ title, children }: PropsWithChildren<{ title: stri
|
|||
);
|
||||
}
|
||||
|
||||
const onError = onlyOnce(handleComponentFailed);
|
||||
export const handleSettingsTabError = onlyOnce(handleComponentFailed);
|
||||
|
||||
export function wrapTab(component: ComponentType, tab: string) {
|
||||
export function wrapTab(component: ComponentType<any>, tab: string) {
|
||||
return ErrorBoundary.wrap(component, {
|
||||
message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
|
||||
onError,
|
||||
onError: handleSettingsTabError,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import type { Settings } from "@api/Settings";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
import { SettingsStore } from "@shared/SettingsStore";
|
||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||
import { ipcMain } from "electron";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
|
||||
|
@ -42,7 +43,22 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string
|
|||
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(() => {
|
||||
try {
|
||||
|
|
|
@ -28,7 +28,7 @@ const VENCORD_SRC_DIR = join(__dirname, "..");
|
|||
|
||||
const execFile = promisify(cpExecFile);
|
||||
|
||||
const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
|
||||
const isFlatpak = process.platform === "linux" && !!process.env.FLATPAK_ID;
|
||||
|
||||
if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;
|
||||
|
||||
|
@ -60,7 +60,8 @@ async function calculateGitChanges() {
|
|||
return commits ? commits.split("\n").map(line => {
|
||||
const [author, hash, ...rest] = line.split("/");
|
||||
return {
|
||||
hash, author, message: rest.join("/")
|
||||
hash, author,
|
||||
message: rest.join("/").split("\n")[0]
|
||||
};
|
||||
}) : [];
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ async function calculateGitChanges() {
|
|||
// github api only sends the long sha
|
||||
hash: c.sha.slice(0, 7),
|
||||
author: c.author.login,
|
||||
message: c.commit.message.substring(c.commit.message.indexOf("\n") + 1)
|
||||
message: c.commit.message.split("\n")[0]
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ export const ALLOWED_PROTOCOLS = [
|
|||
"steam:",
|
||||
"spotify:",
|
||||
"com.epicgames.launcher:",
|
||||
"tidal:"
|
||||
];
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import "./fixBadgeOverflow.css";
|
||||
|
||||
import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
||||
import DonateButton from "@components/DonateButton";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Heart } from "@components/Heart";
|
||||
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { isPluginDev } from "@utils/misc";
|
||||
|
@ -34,14 +37,8 @@ const ContributorBadge: ProfileBadge = {
|
|||
description: "Vencord Contributor",
|
||||
image: CONTRIBUTOR_BADGE,
|
||||
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),
|
||||
link: "https://github.com/Vendicated/Vencord"
|
||||
onClick: (_, { user }) => openContributorModal(user)
|
||||
};
|
||||
|
||||
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
|
||||
|
@ -65,7 +62,7 @@ export default definePlugin({
|
|||
patches: [
|
||||
/* Patch the badge list component on user profiles */
|
||||
{
|
||||
find: "Messages.PROFILE_USER_BADGES,role:",
|
||||
find: 'id:"premium",',
|
||||
replacement: [
|
||||
{
|
||||
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
|
||||
|
@ -79,13 +76,13 @@ export default definePlugin({
|
|||
},
|
||||
// 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) :"
|
||||
},
|
||||
// conditionally override their onClick with badge.onClick if it exists
|
||||
{
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
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({
|
||||
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],
|
||||
required: true,
|
||||
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "AnalyticsActionHandlers.handle",
|
||||
predicate: () => settings.store.disableAnalytics,
|
||||
replacement: {
|
||||
match: /^.+$/,
|
||||
replace: "()=>{}",
|
||||
|
@ -44,11 +58,11 @@ export default definePlugin({
|
|||
replacement: [
|
||||
{
|
||||
match: /this\._intervalId=/,
|
||||
replace: "this._intervalId=undefined&&"
|
||||
replace: "this._intervalId=void 0&&"
|
||||
},
|
||||
{
|
||||
match: /(increment\(\i\){)/,
|
||||
replace: "$1return;"
|
||||
match: /(?:increment|distribution)\(\i(?:,\i)?\){/g,
|
||||
replace: "$&return;"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -17,57 +17,72 @@
|
|||
*/
|
||||
|
||||
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 definePlugin, { OptionType } from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
import { i18n, React } from "@webpack/common";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
|
||||
type SectionType = "HEADER" | "DIVIDER" | "CUSTOM";
|
||||
type SectionTypes = Record<SectionType, SectionType>;
|
||||
|
||||
export default definePlugin({
|
||||
name: "Settings",
|
||||
description: "Adds Settings UI and debug info",
|
||||
authors: [Devs.Ven, Devs.Megu],
|
||||
required: true,
|
||||
|
||||
patches: [{
|
||||
patches: [
|
||||
{
|
||||
find: ".versionHash",
|
||||
replacement: [
|
||||
{
|
||||
match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
|
||||
match: /\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
|
||||
replace: (m, component, props) => {
|
||||
props = props.replace(/children:\[.+\]/, "");
|
||||
return `${m},Vencord.Plugins.plugins.Settings.makeInfoElements(${component}, ${props})`;
|
||||
}
|
||||
}
|
||||
]
|
||||
}, {
|
||||
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\}/;
|
||||
return `${m},$self.makeInfoElements(${component}, ${props})`;
|
||||
}
|
||||
},
|
||||
replace: "...$self.makeSettingsCategories($1),$&"
|
||||
{
|
||||
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;"
|
||||
}
|
||||
}],
|
||||
}
|
||||
],
|
||||
|
||||
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[],
|
||||
customSections: [] as ((SectionTypes: SectionTypes) => any)[],
|
||||
|
||||
makeSettingsCategories(SectionTypes: Record<string, unknown>) {
|
||||
makeSettingsCategories(SectionTypes: SectionTypes) {
|
||||
return [
|
||||
{
|
||||
section: SectionTypes.HEADER,
|
||||
|
@ -77,43 +92,43 @@ export default definePlugin({
|
|||
{
|
||||
section: "VencordSettings",
|
||||
label: "Vencord",
|
||||
element: require("@components/VencordSettings/VencordTab").default,
|
||||
element: VencordTab,
|
||||
className: "vc-settings"
|
||||
},
|
||||
{
|
||||
section: "VencordPlugins",
|
||||
label: "Plugins",
|
||||
element: require("@components/VencordSettings/PluginsTab").default,
|
||||
element: PluginsTab,
|
||||
className: "vc-plugins"
|
||||
},
|
||||
{
|
||||
section: "VencordThemes",
|
||||
label: "Themes",
|
||||
element: require("@components/VencordSettings/ThemesTab").default,
|
||||
element: ThemesTab,
|
||||
className: "vc-themes"
|
||||
},
|
||||
!IS_UPDATER_DISABLED && {
|
||||
section: "VencordUpdater",
|
||||
label: "Updater",
|
||||
element: require("@components/VencordSettings/UpdaterTab").default,
|
||||
element: UpdaterTab,
|
||||
className: "vc-updater"
|
||||
},
|
||||
{
|
||||
section: "VencordCloud",
|
||||
label: "Cloud",
|
||||
element: require("@components/VencordSettings/CloudTab").default,
|
||||
element: CloudTab,
|
||||
className: "vc-cloud"
|
||||
},
|
||||
{
|
||||
section: "VencordSettingsSync",
|
||||
label: "Backup & Restore",
|
||||
element: require("@components/VencordSettings/BackupAndRestoreTab").default,
|
||||
element: BackupAndRestoreTab,
|
||||
className: "vc-backup-restore"
|
||||
},
|
||||
IS_DEV && {
|
||||
section: "VencordPatchHelper",
|
||||
label: "Patch Helper",
|
||||
element: require("@components/VencordSettings/PatchHelperTab").default,
|
||||
element: PatchHelperTab,
|
||||
className: "vc-patch-helper"
|
||||
},
|
||||
...this.customSections.map(func => func(SectionTypes)),
|
||||
|
@ -123,19 +138,63 @@ export default definePlugin({
|
|||
].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: {
|
||||
settingsLocation: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Where to put the Vencord settings section",
|
||||
options: [
|
||||
{ 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: "Above Activity Settings", value: "aboveActivity", default: true },
|
||||
{ label: "Above Activity Settings", value: "aboveActivity" },
|
||||
{ label: "Below Activity Settings", value: "belowActivity" },
|
||||
{ label: "At the very bottom", value: "bottom" },
|
||||
],
|
||||
restartNeeded: true
|
||||
]
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -162,15 +221,24 @@ export default definePlugin({
|
|||
return "";
|
||||
},
|
||||
|
||||
makeInfoElements(Component: React.ComponentType<React.PropsWithChildren>, props: React.PropsWithChildren) {
|
||||
getInfoRows() {
|
||||
const { electronVersion, chromiumVersion, additionalInfo } = this;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component {...props}>Vencord {gitHash}{additionalInfo}</Component>
|
||||
{electronVersion && <Component {...props}>Electron {electronVersion}</Component>}
|
||||
{chromiumVersion && <Component {...props}>Chromium {chromiumVersion}</Component>}
|
||||
</>
|
||||
const rows = [`Vencord ${gitHash}${additionalInfo}`];
|
||||
|
||||
if (electronVersion) rows.push(`Electron ${electronVersion}`);
|
||||
if (chromiumVersion) rows.push(`Chromium ${chromiumVersion}`);
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
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 { Margins } from "@utils/margins";
|
||||
import { isPluginDev } from "@utils/misc";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { makeCodeblock } from "@utils/text";
|
||||
import definePlugin from "@utils/types";
|
||||
import { isOutdated } from "@utils/updater";
|
||||
import { Alerts, Forms, UserStore } from "@webpack/common";
|
||||
import { isOutdated, update } from "@utils/updater";
|
||||
import { Alerts, Card, ChannelStore, Forms, GuildMemberStore, NavigationRouter, Parser, RelationshipStore, UserStore } from "@webpack/common";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
import plugins from "~plugins";
|
||||
|
||||
import settings from "./settings";
|
||||
|
||||
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
|
||||
const VENCORD_GUILD_ID = "1015060230222131221";
|
||||
|
||||
const AllowedChannelIds = [
|
||||
SUPPORT_CHANNEL_ID,
|
||||
|
@ -37,6 +41,12 @@ const AllowedChannelIds = [
|
|||
"1033680203433660458", // Vencord > #v
|
||||
];
|
||||
|
||||
const TrustedRolesIds = [
|
||||
"1026534353167208489", // contributor
|
||||
"1026504932959977532", // regular
|
||||
"1042507929485586532", // donor
|
||||
];
|
||||
|
||||
export default definePlugin({
|
||||
name: "SupportHelper",
|
||||
required: true,
|
||||
|
@ -44,6 +54,14 @@ export default definePlugin({
|
|||
authors: [Devs.Ven],
|
||||
dependencies: ["CommandsAPI"],
|
||||
|
||||
patches: [{
|
||||
find: ".BEGINNING_DM.format",
|
||||
replacement: {
|
||||
match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
|
||||
replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
|
||||
}
|
||||
}],
|
||||
|
||||
commands: [{
|
||||
name: "vencord-debug",
|
||||
description: "Send Vencord Debug info",
|
||||
|
@ -64,15 +82,13 @@ export default definePlugin({
|
|||
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
|
||||
|
||||
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 = {
|
||||
Vencord: `v${VERSION} • ${gitHash}${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
||||
"Discord Branch": RELEASE_CHANNEL,
|
||||
Client: client,
|
||||
Platform: window.navigator.platform,
|
||||
Outdated: isOutdated,
|
||||
OpenAsar: "openasar" in window,
|
||||
Vencord:
|
||||
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
|
||||
`${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
||||
Client: `${RELEASE_CHANNEL} ~ ${client}`,
|
||||
Platform: window.navigator.platform
|
||||
};
|
||||
|
||||
if (IS_DISCORD_DESKTOP) {
|
||||
|
@ -80,11 +96,10 @@ export default definePlugin({
|
|||
}
|
||||
|
||||
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}):
|
||||
${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "))}
|
||||
Enabled Plugins (${enabledPlugins.length}):
|
||||
${makeCodeblock(enabledPlugins.join(", "))}
|
||||
`;
|
||||
|
||||
return {
|
||||
|
@ -97,24 +112,75 @@ ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "
|
|||
async CHANNEL_SELECT({ channelId }) {
|
||||
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)) {
|
||||
const rememberDismiss = () => DataStore.set(REMEMBER_DISMISS_KEY, gitHash);
|
||||
|
||||
Alerts.show({
|
||||
if (isOutdated) {
|
||||
return Alerts.show({
|
||||
title: "Hold on!",
|
||||
body: <div>
|
||||
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
Please first update using the Updater Page in Settings, or use the VencordInstaller (Update Vencord Button)
|
||||
to do so, in case you can't access the Updater page.
|
||||
<Forms.FormText className={Margins.top8}>
|
||||
Please first update before asking for support!
|
||||
</Forms.FormText>
|
||||
</div>,
|
||||
onCancel: rememberDismiss,
|
||||
onConfirm: rememberDismiss
|
||||
onCancel: () => openUpdaterModal!(),
|
||||
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 definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { FluxDispatcher, i18n } from "@webpack/common";
|
||||
import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
|
||||
|
||||
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
|
||||
{
|
||||
match: /(useStateFromStoresArray\).{0,25}let \i)=(\i\.\i.getGuildsTree\(\))/,
|
||||
replace: (_, rest, guildsTree) => `${rest}=$self.getGuildTree(!!arguments[0].isBetterFolders,${guildsTree},arguments[0].betterFoldersExpandedIds)`
|
||||
match: /\[(\i)\]=(\(0,\i\.useStateFromStoresArray\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
|
||||
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
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
match: /unreadMentionsIndicatorBottom,barClassName.+?}\)\]/,
|
||||
match: /unreadMentionsIndicatorBottom,.+?}\)\]/,
|
||||
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
|
||||
},
|
||||
// Export the isBetterFolders variable to the folders component
|
||||
|
@ -209,7 +209,7 @@ export default definePlugin({
|
|||
predicate: () => settings.store.closeAllHomeButton,
|
||||
replacement: {
|
||||
// Close all folders when clicking the home button
|
||||
match: /(?<=onClick:\(\)=>{)(?=.{0,200}"discodo")/,
|
||||
match: /(?<=onClick:\(\)=>{)(?=.{0,300}"discodo")/,
|
||||
replace: "$self.closeFolders();"
|
||||
}
|
||||
}
|
||||
|
@ -252,19 +252,21 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
|
||||
getGuildTree(isBetterFolders: boolean, oldTree: any, expandedFolderIds?: Set<any>) {
|
||||
if (!isBetterFolders || expandedFolderIds == null) return oldTree;
|
||||
getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set<any>) {
|
||||
return useMemo(() => {
|
||||
if (!isBetterFolders || expandedFolderIds == null) return originalTree;
|
||||
|
||||
const newTree = new GuildsTree();
|
||||
// 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
|
||||
newTree.nodes = Object.fromEntries(
|
||||
Object.entries(oldTree.nodes)
|
||||
Object.entries(originalTree.nodes)
|
||||
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
|
||||
);
|
||||
|
||||
return newTree;
|
||||
}, [isBetterFolders, originalTree, expandedFolderIds]);
|
||||
},
|
||||
|
||||
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
|
||||
|
@ -279,7 +281,7 @@ export default definePlugin({
|
|||
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
|
||||
return child => {
|
||||
if (isBetterFolders) {
|
||||
return "onScroll" in child.props;
|
||||
return child?.props?.onScroll != null;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { canonicalizeMatch } from "@utils/patches";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
@ -60,7 +61,7 @@ export default definePlugin({
|
|||
find: ".popularApplicationCommandIds,",
|
||||
replacement: {
|
||||
match: /lastSection:(!?\i)}\),/,
|
||||
replace: "$&$self.patchPadding($1),"
|
||||
replace: "$&$self.patchPadding({lastSection:$1}),"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -80,10 +81,10 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
|
||||
patchPadding(lastSection: any) {
|
||||
if (!lastSection) return;
|
||||
patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
|
||||
if (!lastSection) return null;
|
||||
return (
|
||||
<div className={UserPopoutSectionCssClasses.lastSection} ></div>
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
5
src/plugins/betterSessions/README.md
Normal file
5
src/plugins/betterSessions/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# BetterSessions
|
||||
|
||||
Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.
|
||||
|
||||

|
37
src/plugins/betterSessions/components/RenameButton.tsx
Normal file
37
src/plugins/betterSessions/components/RenameButton.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { openModal } from "@utils/modal";
|
||||
import { Button } from "@webpack/common";
|
||||
|
||||
import { SessionInfo } from "../types";
|
||||
import { RenameModal } from "./RenameModal";
|
||||
|
||||
export function RenameButton({ session, state }: { session: SessionInfo["session"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {
|
||||
return (
|
||||
<Button
|
||||
look={Button.Looks.LINK}
|
||||
color={Button.Colors.LINK}
|
||||
size={Button.Sizes.NONE}
|
||||
style={{
|
||||
paddingTop: "0px",
|
||||
paddingBottom: "0px",
|
||||
top: "-2px"
|
||||
}}
|
||||
onClick={() =>
|
||||
openModal(props => (
|
||||
<RenameModal
|
||||
props={props}
|
||||
session={session}
|
||||
state={state}
|
||||
/>
|
||||
))
|
||||
}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
);
|
||||
}
|
94
src/plugins/betterSessions/components/RenameModal.tsx
Normal file
94
src/plugins/betterSessions/components/RenameModal.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
|
||||
import { Button, Forms, React, TextInput } from "@webpack/common";
|
||||
import { KeyboardEvent } from "react";
|
||||
|
||||
import { SessionInfo } from "../types";
|
||||
import { getDefaultName, savedSessionsCache, saveSessionsToDataStore } from "../utils";
|
||||
|
||||
export function RenameModal({ props, session, state }: { props: ModalProps, session: SessionInfo["session"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {
|
||||
const [title, setTitle] = state;
|
||||
const [value, setValue] = React.useState(savedSessionsCache.get(session.id_hash)?.name ?? "");
|
||||
|
||||
function onSaveClick() {
|
||||
savedSessionsCache.set(session.id_hash, { name: value, isNew: false });
|
||||
if (value !== "") {
|
||||
setTitle(`${value}*`);
|
||||
} else {
|
||||
setTitle(getDefaultName(session.client_info));
|
||||
}
|
||||
|
||||
saveSessionsToDataStore();
|
||||
props.onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeader>
|
||||
<Forms.FormTitle tag="h4">Rename</Forms.FormTitle>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>New device name</Forms.FormTitle>
|
||||
<TextInput
|
||||
style={{ marginBottom: "10px" }}
|
||||
placeholder={getDefaultName(session.client_info)}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
onSaveClick();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
marginBottom: "20px",
|
||||
paddingLeft: "1px",
|
||||
paddingRight: "1px",
|
||||
opacity: 0.6
|
||||
}}
|
||||
look={Button.Looks.LINK}
|
||||
color={Button.Colors.LINK}
|
||||
size={Button.Sizes.NONE}
|
||||
onClick={() => setValue("")}
|
||||
>
|
||||
Reset Name
|
||||
</Button>
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color={Button.Colors.BRAND}
|
||||
onClick={onSaveClick}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.LINK}
|
||||
onClick={() => props.onClose()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot >
|
||||
);
|
||||
}
|
106
src/plugins/betterSessions/components/icons.tsx
Normal file
106
src/plugins/betterSessions/components/icons.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 { LazyComponent } from "@utils/react";
|
||||
import { findByCode } from "@webpack";
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export const DiscordIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ChromeIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M188.8,255.93A67.2,67.2,0,1,0,256,188.75,67.38,67.38,0,0,0,188.8,255.93Z" />
|
||||
<path d="M476.75,217.79s0,0,0,.05a206.63,206.63,0,0,0-7-28.84h-.11a202.16,202.16,0,0,1,7.07,29h0a203.5,203.5,0,0,0-7.07-29H314.24c19.05,17,31.36,40.17,31.36,67.05a86.55,86.55,0,0,1-12.31,44.73L231,478.45a2.44,2.44,0,0,1,0,.27V479h0v-.26A224,224,0,0,0,256,480c6.84,0,13.61-.39,20.3-1a222.91,222.91,0,0,0,29.78-4.74C405.68,451.52,480,362.4,480,255.94A225.25,225.25,0,0,0,476.75,217.79Z" />
|
||||
<path d="M256,345.5c-33.6,0-61.6-17.91-77.29-44.79L76,123.05l-.14-.24A224,224,0,0,0,207.4,474.55l0-.05,77.69-134.6A84.13,84.13,0,0,1,256,345.5Z" />
|
||||
<path d="M91.29,104.57l77.35,133.25A89.19,89.19,0,0,1,256,166H461.17a246.51,246.51,0,0,0-25.78-43.94l.12.08A245.26,245.26,0,0,1,461.17,166h.17a245.91,245.91,0,0,0-25.66-44,2.63,2.63,0,0,1-.35-.26A223.93,223.93,0,0,0,91.14,104.34l.14.24Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const EdgeIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M21.86 17.86q.14 0 .25.12.1.13.1.25t-.11.33l-.32.46-.43.53-.44.5q-.21.25-.38.42l-.22.23q-.58.53-1.34 1.04-.76.51-1.6.91-.86.4-1.74.64t-1.67.24q-.9 0-1.69-.28-.8-.28-1.48-.78-.68-.5-1.22-1.17-.53-.66-.92-1.44-.38-.77-.58-1.6-.2-.83-.2-1.67 0-1 .32-1.96.33-.97.87-1.8.14.95.55 1.77.41.82 1.02 1.5.6.68 1.38 1.21.78.54 1.64.9.86.36 1.77.56.92.2 1.8.2 1.12 0 2.18-.24 1.06-.23 2.06-.72l.2-.1.2-.05zm-15.5-1.27q0 1.1.27 2.15.27 1.06.78 2.03.51.96 1.24 1.77.74.82 1.66 1.4-1.47-.2-2.8-.74-1.33-.55-2.48-1.37-1.15-.83-2.08-1.9-.92-1.07-1.58-2.33T.36 14.94Q0 13.54 0 12.06q0-.81.32-1.49.31-.68.83-1.23.53-.55 1.2-.96.66-.4 1.35-.66.74-.27 1.5-.39.78-.12 1.55-.12.7 0 1.42.1.72.12 1.4.35.68.23 1.32.57.63.35 1.16.83-.35 0-.7.07-.33.07-.65.23v-.02q-.63.28-1.2.74-.57.46-1.05 1.04-.48.58-.87 1.26-.38.67-.65 1.39-.27.71-.42 1.44-.15.72-.15 1.38zM11.96.06q1.7 0 3.33.39 1.63.38 3.07 1.15 1.43.77 2.62 1.93 1.18 1.16 1.98 2.7.49.94.76 1.96.28 1 .28 2.08 0 .89-.23 1.7-.24.8-.69 1.48-.45.68-1.1 1.22-.64.53-1.45.88-.54.24-1.11.36-.58.13-1.16.13-.42 0-.97-.03-.54-.03-1.1-.12-.55-.1-1.05-.28-.5-.19-.84-.5-.12-.09-.23-.24-.1-.16-.1-.33 0-.15.16-.35.16-.2.35-.5.2-.28.36-.68.16-.4.16-.95 0-1.06-.4-1.96-.4-.91-1.06-1.64-.66-.74-1.52-1.28-.86-.55-1.79-.89-.84-.3-1.72-.44-.87-.14-1.76-.14-1.55 0-3.06.45T.94 7.55q.71-1.74 1.81-3.13 1.1-1.38 2.52-2.35Q6.68 1.1 8.37.58q1.7-.52 3.58-.52Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FirefoxIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M130.22 127.548C130.38 127.558 130.3 127.558 130.22 127.548V127.548ZM481.64 172.898C471.03 147.398 449.56 119.898 432.7 111.168C446.42 138.058 454.37 165.048 457.4 185.168C457.405 185.306 457.422 185.443 457.45 185.578C429.87 116.828 383.098 89.1089 344.9 28.7479C329.908 5.05792 333.976 3.51792 331.82 4.08792L331.7 4.15792C284.99 30.1109 256.365 82.5289 249.12 126.898C232.503 127.771 216.219 131.895 201.19 139.035C199.838 139.649 198.736 140.706 198.066 142.031C197.396 143.356 197.199 144.87 197.506 146.323C197.7 147.162 198.068 147.951 198.586 148.639C199.103 149.327 199.76 149.899 200.512 150.318C201.264 150.737 202.096 150.993 202.954 151.071C203.811 151.148 204.676 151.045 205.491 150.768L206.011 150.558C221.511 143.255 238.408 139.393 255.541 139.238C318.369 138.669 352.698 183.262 363.161 201.528C350.161 192.378 326.811 183.338 304.341 187.248C392.081 231.108 368.541 381.784 246.951 376.448C187.487 373.838 149.881 325.467 146.421 285.648C146.421 285.648 157.671 243.698 227.041 243.698C234.541 243.698 255.971 222.778 256.371 216.698C256.281 214.698 213.836 197.822 197.281 181.518C188.434 172.805 184.229 168.611 180.511 165.458C178.499 163.75 176.392 162.158 174.201 160.688C168.638 141.231 168.399 120.638 173.51 101.058C148.45 112.468 128.96 130.508 114.8 146.428H114.68C105.01 134.178 105.68 93.7779 106.25 85.3479C106.13 84.8179 99.022 89.0159 98.1 89.6579C89.5342 95.7103 81.5528 102.55 74.26 110.088C57.969 126.688 30.128 160.242 18.76 211.318C14.224 231.701 12 255.739 12 263.618C12 398.318 121.21 507.508 255.92 507.508C376.56 507.508 478.939 420.281 496.35 304.888C507.922 228.192 481.64 173.82 481.64 172.898Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IEIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M483.049 159.706c10.855-24.575 21.424-60.438 21.424-87.871 0-72.722-79.641-98.371-209.673-38.577-107.632-7.181-211.221 73.67-237.098 186.457 30.852-34.862 78.271-82.298 121.977-101.158C125.404 166.85 79.128 228.002 43.992 291.725 23.246 329.651 0 390.94 0 436.747c0 98.575 92.854 86.5 180.251 42.006 31.423 15.43 66.559 15.573 101.695 15.573 97.124 0 184.249-54.294 216.814-146.022H377.927c-52.509 88.593-196.819 52.996-196.819-47.436H509.9c6.407-43.581-1.655-95.715-26.851-141.162zM64.559 346.877c17.711 51.15 53.703 95.871 100.266 123.304-88.741 48.94-173.267 29.096-100.266-123.304zm115.977-108.873c2-55.151 50.276-94.871 103.98-94.871 53.418 0 101.981 39.72 103.981 94.871H180.536zm184.536-187.6c21.425-10.287 48.563-22.003 72.558-22.003 31.422 0 54.274 21.717 54.274 53.722 0 20.003-7.427 49.007-14.569 67.867-26.28-42.292-65.986-81.584-112.263-99.586z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const OperaIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 496 512"
|
||||
>
|
||||
<path d="M313.9 32.7c-170.2 0-252.6 223.8-147.5 355.1 36.5 45.4 88.6 75.6 147.5 75.6 36.3 0 70.3-11.1 99.4-30.4-43.8 39.2-101.9 63-165.3 63-3.9 0-8 0-11.9-.3C104.6 489.6 0 381.1 0 248 0 111 111 0 248 0h.8c63.1.3 120.7 24.1 164.4 63.1-29-19.4-63.1-30.4-99.3-30.4zm101.8 397.7c-40.9 24.7-90.7 23.6-132-5.8 56.2-20.5 97.7-91.6 97.7-176.6 0-84.7-41.2-155.8-97.4-176.6 41.8-29.2 91.2-30.3 132.9-5 105.9 98.7 105.5 265.7-1.2 364z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SafariIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M274.69,274.69l-37.38-37.38L166,346ZM256,8C119,8,8,119,8,256S119,504,256,504,504,393,504,256,393,8,256,8ZM411.85,182.79l14.78-6.13A8,8,0,0,1,437.08,181h0a8,8,0,0,1-4.33,10.46L418,197.57a8,8,0,0,1-10.45-4.33h0A8,8,0,0,1,411.85,182.79ZM314.43,94l6.12-14.78A8,8,0,0,1,331,74.92h0a8,8,0,0,1,4.33,10.45l-6.13,14.78a8,8,0,0,1-10.45,4.33h0A8,8,0,0,1,314.43,94ZM256,60h0a8,8,0,0,1,8,8V84a8,8,0,0,1-8,8h0a8,8,0,0,1-8-8V68A8,8,0,0,1,256,60ZM181,74.92a8,8,0,0,1,10.46,4.33L197.57,94a8,8,0,1,1-14.78,6.12l-6.13-14.78A8,8,0,0,1,181,74.92Zm-63.58,42.49h0a8,8,0,0,1,11.31,0L140,128.72A8,8,0,0,1,140,140h0a8,8,0,0,1-11.31,0l-11.31-11.31A8,8,0,0,1,117.41,117.41ZM60,256h0a8,8,0,0,1,8-8H84a8,8,0,0,1,8,8h0a8,8,0,0,1-8,8H68A8,8,0,0,1,60,256Zm40.15,73.21-14.78,6.13A8,8,0,0,1,74.92,331h0a8,8,0,0,1,4.33-10.46L94,314.43a8,8,0,0,1,10.45,4.33h0A8,8,0,0,1,100.15,329.21Zm4.33-136h0A8,8,0,0,1,94,197.57l-14.78-6.12A8,8,0,0,1,74.92,181h0a8,8,0,0,1,10.45-4.33l14.78,6.13A8,8,0,0,1,104.48,193.24ZM197.57,418l-6.12,14.78a8,8,0,0,1-14.79-6.12l6.13-14.78A8,8,0,1,1,197.57,418ZM264,444a8,8,0,0,1-8,8h0a8,8,0,0,1-8-8V428a8,8,0,0,1,8-8h0a8,8,0,0,1,8,8Zm67-6.92h0a8,8,0,0,1-10.46-4.33L314.43,418a8,8,0,0,1,4.33-10.45h0a8,8,0,0,1,10.45,4.33l6.13,14.78A8,8,0,0,1,331,437.08Zm63.58-42.49h0a8,8,0,0,1-11.31,0L372,383.28A8,8,0,0,1,372,372h0a8,8,0,0,1,11.31,0l11.31,11.31A8,8,0,0,1,394.59,394.59ZM286.25,286.25,110.34,401.66,225.75,225.75,401.66,110.34ZM437.08,331h0a8,8,0,0,1-10.45,4.33l-14.78-6.13a8,8,0,0,1-4.33-10.45h0A8,8,0,0,1,418,314.43l14.78,6.12A8,8,0,0,1,437.08,331ZM444,264H428a8,8,0,0,1-8-8h0a8,8,0,0,1,8-8h16a8,8,0,0,1,8,8h0A8,8,0,0,1,444,264Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const UnknownIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path fill-rule="evenodd" d="M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 0 1-.5.5h-.77a.5.5 0 0 1-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777ZM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MobileIcon = LazyComponent(() => findByCode("M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38"));
|
227
src/plugins/betterSessions/index.tsx
Normal file
227
src/plugins/betterSessions/index.tsx
Normal file
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* 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 { showNotification } from "@api/Notifications";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
|
||||
import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
|
||||
|
||||
import { RenameButton } from "./components/RenameButton";
|
||||
import { Session, SessionInfo } from "./types";
|
||||
import { fetchNamesFromDataStore, getDefaultName, GetOsColor, GetPlatformIcon, savedSessionsCache, saveSessionsToDataStore } from "./utils";
|
||||
|
||||
const AuthSessionsStore = findStoreLazy("AuthSessionsStore");
|
||||
const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open");
|
||||
|
||||
const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer");
|
||||
const SessionIconClasses = findByPropsLazy("sessionIcon");
|
||||
|
||||
const BlobMask = findExportedComponentLazy("BlobMask");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
backgroundCheck: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Check for new sessions in the background, and display notifications when they are detected",
|
||||
default: false,
|
||||
restartNeeded: true
|
||||
},
|
||||
checkInterval: {
|
||||
description: "How often to check for new sessions in the background (if enabled), in minutes",
|
||||
type: OptionType.NUMBER,
|
||||
default: 20,
|
||||
restartNeeded: true
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterSessions",
|
||||
description: "Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.",
|
||||
authors: [Devs.amia],
|
||||
|
||||
settings: settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "Messages.AUTH_SESSIONS_SESSION_LOG_OUT",
|
||||
replacement: [
|
||||
// Replace children with a single label with state
|
||||
{
|
||||
match: /({variant:"eyebrow",className:\i\.sessionInfoRow,children:).{70,110}{children:"\\xb7"}\),\(0,\i\.\i\)\("span",{children:\i\[\d+\]}\)\]}\)\]/,
|
||||
replace: "$1$self.renderName(arguments[0])"
|
||||
},
|
||||
{
|
||||
match: /({variant:"text-sm\/medium",className:\i\.sessionInfoRow,children:.{70,110}{children:"\\xb7"}\),\(0,\i\.\i\)\("span",{children:)(\i\[\d+\])}/,
|
||||
replace: "$1$self.renderTimestamp({ ...arguments[0], timeLabel: $2 })}"
|
||||
},
|
||||
// Replace the icon
|
||||
{
|
||||
match: /\.currentSession:null\),children:\[(?<=,icon:(\i)\}.+?)/,
|
||||
replace: "$& $self.renderIcon({ ...arguments[0], DeviceIcon: $1 }), false &&"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// Add the ability to change BlobMask's lower badge height
|
||||
// (it allows changing width so we can mirror that logic)
|
||||
find: "this.getBadgePositionInterpolation(",
|
||||
replacement: {
|
||||
match: /(\i\.animated\.rect,{id:\i,x:48-\(\i\+8\)\+4,y:)28(,width:\i\+8,height:)24,/,
|
||||
replace: (_, leftPart, rightPart) => `${leftPart} 48 - ((this.props.lowerBadgeHeight ?? 16) + 8) + 4 ${rightPart} (this.props.lowerBadgeHeight ?? 16) + 8,`
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
renderName: ErrorBoundary.wrap(({ session }: SessionInfo) => {
|
||||
const savedSession = savedSessionsCache.get(session.id_hash);
|
||||
|
||||
const state = React.useState(savedSession?.name ? `${savedSession.name}*` : getDefaultName(session.client_info));
|
||||
const [title, setTitle] = state;
|
||||
|
||||
// Show a "NEW" badge if the session is seen for the first time
|
||||
return (
|
||||
<>
|
||||
<span>{title}</span>
|
||||
{(savedSession == null || savedSession.isNew) && (
|
||||
<div
|
||||
className="vc-plugins-badge"
|
||||
style={{
|
||||
backgroundColor: "#ED4245",
|
||||
marginLeft: "2px"
|
||||
}}
|
||||
>
|
||||
NEW
|
||||
</div>
|
||||
)}
|
||||
<RenameButton session={session} state={state} />
|
||||
</>
|
||||
);
|
||||
}, { noop: true }),
|
||||
|
||||
renderTimestamp: ErrorBoundary.wrap(({ session, timeLabel }: { session: Session, timeLabel: string; }) => {
|
||||
return (
|
||||
<Tooltip text={session.approx_last_used_time.toLocaleString()} tooltipClassName={TimestampClasses.timestampTooltip}>
|
||||
{props => (
|
||||
<span {...props} className={TimestampClasses.timestamp}>
|
||||
{timeLabel}
|
||||
</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}, { noop: true }),
|
||||
|
||||
renderIcon: ErrorBoundary.wrap(({ session, DeviceIcon }: { session: Session, DeviceIcon: React.ComponentType<any>; }) => {
|
||||
const PlatformIcon = GetPlatformIcon(session.client_info.platform);
|
||||
|
||||
return (
|
||||
<BlobMask
|
||||
style={{ cursor: "unset" }}
|
||||
selected={false}
|
||||
lowerBadge={
|
||||
<div
|
||||
style={{
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--interactive-normal)",
|
||||
color: "var(--background-secondary)",
|
||||
}}
|
||||
>
|
||||
<PlatformIcon width={14} height={14} />
|
||||
</div>
|
||||
}
|
||||
lowerBadgeWidth={20}
|
||||
lowerBadgeHeight={20}
|
||||
>
|
||||
<div
|
||||
className={SessionIconClasses.sessionIcon}
|
||||
style={{ backgroundColor: GetOsColor(session.client_info.os) }}
|
||||
>
|
||||
<DeviceIcon width={28} height={28} />
|
||||
</div>
|
||||
</BlobMask>
|
||||
);
|
||||
}, { noop: true }),
|
||||
|
||||
async checkNewSessions() {
|
||||
const data = await RestAPI.get({
|
||||
url: Constants.Endpoints.AUTH_SESSIONS
|
||||
});
|
||||
|
||||
for (const session of data.body.user_sessions) {
|
||||
if (savedSessionsCache.has(session.id_hash)) continue;
|
||||
|
||||
savedSessionsCache.set(session.id_hash, { name: "", isNew: true });
|
||||
showNotification({
|
||||
title: "BetterSessions",
|
||||
body: `New session:\n${session.client_info.os} · ${session.client_info.platform} · ${session.client_info.location}`,
|
||||
permanent: true,
|
||||
onClick: () => UserSettingsModal.open("Sessions")
|
||||
});
|
||||
}
|
||||
|
||||
saveSessionsToDataStore();
|
||||
},
|
||||
|
||||
flux: {
|
||||
USER_SETTINGS_ACCOUNT_RESET_AND_CLOSE_FORM() {
|
||||
const lastFetchedHashes: string[] = AuthSessionsStore.getSessions().map((session: SessionInfo["session"]) => session.id_hash);
|
||||
|
||||
// Add new sessions to cache
|
||||
lastFetchedHashes.forEach(idHash => {
|
||||
if (!savedSessionsCache.has(idHash)) savedSessionsCache.set(idHash, { name: "", isNew: false });
|
||||
});
|
||||
|
||||
// Delete removed sessions from cache
|
||||
if (lastFetchedHashes.length > 0) {
|
||||
savedSessionsCache.forEach((_, idHash) => {
|
||||
if (!lastFetchedHashes.includes(idHash)) savedSessionsCache.delete(idHash);
|
||||
});
|
||||
}
|
||||
|
||||
// Dismiss the "NEW" badge of all sessions.
|
||||
// Since the only way for a session to be marked as "NEW" is going to the Devices tab,
|
||||
// closing the settings means they've been viewed and are no longer considered new.
|
||||
savedSessionsCache.forEach(data => {
|
||||
data.isNew = false;
|
||||
});
|
||||
saveSessionsToDataStore();
|
||||
}
|
||||
},
|
||||
|
||||
async start() {
|
||||
await fetchNamesFromDataStore();
|
||||
|
||||
this.checkNewSessions();
|
||||
if (settings.store.backgroundCheck) {
|
||||
this.checkInterval = setInterval(this.checkNewSessions, settings.store.checkInterval * 60 * 1000);
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
clearInterval(this.checkInterval);
|
||||
}
|
||||
});
|
|
@ -16,20 +16,17 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
export interface SessionInfo {
|
||||
session: {
|
||||
id_hash: string;
|
||||
approx_last_used_time: Date;
|
||||
client_info: {
|
||||
os: string;
|
||||
platform: string;
|
||||
location: string;
|
||||
};
|
||||
},
|
||||
current?: boolean;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "ShowTimeouts",
|
||||
description: "Display member timeout icons in chat regardless of permissions.",
|
||||
authors: [Devs.Dolfies],
|
||||
patches: [
|
||||
{
|
||||
find: "showCommunicationDisabledStyles",
|
||||
replacement: {
|
||||
match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/,
|
||||
replace: "",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
export type Session = SessionInfo["session"];
|
90
src/plugins/betterSessions/utils.ts
Normal file
90
src/plugins/betterSessions/utils.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { DataStore } from "@api/index";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
import { ChromeIcon, DiscordIcon, EdgeIcon, FirefoxIcon, IEIcon, MobileIcon, OperaIcon, SafariIcon, UnknownIcon } from "./components/icons";
|
||||
import { SessionInfo } from "./types";
|
||||
|
||||
const getDataKey = () => `BetterSessions_savedSessions_${UserStore.getCurrentUser().id}`;
|
||||
|
||||
export const savedSessionsCache: Map<string, { name: string, isNew: boolean; }> = new Map();
|
||||
|
||||
export function getDefaultName(clientInfo: SessionInfo["session"]["client_info"]) {
|
||||
return `${clientInfo.os} · ${clientInfo.platform}`;
|
||||
}
|
||||
|
||||
export function saveSessionsToDataStore() {
|
||||
return DataStore.set(getDataKey(), savedSessionsCache);
|
||||
}
|
||||
|
||||
export async function fetchNamesFromDataStore() {
|
||||
const savedSessions = await DataStore.get<Map<string, { name: string, isNew: boolean; }>>(getDataKey()) || new Map();
|
||||
savedSessions.forEach((data, idHash) => {
|
||||
savedSessionsCache.set(idHash, data);
|
||||
});
|
||||
}
|
||||
|
||||
export function GetOsColor(os: string) {
|
||||
switch (os) {
|
||||
case "Windows Mobile":
|
||||
case "Windows":
|
||||
return "#55a6ef"; // Light blue
|
||||
case "Linux":
|
||||
return "#cdcd31"; // Yellow
|
||||
case "Android":
|
||||
return "#7bc958"; // Green
|
||||
case "Mac OS X":
|
||||
case "iOS":
|
||||
return ""; // Default to white/black (theme-dependent)
|
||||
default:
|
||||
return "#f3799a"; // Pink
|
||||
}
|
||||
}
|
||||
|
||||
export function GetPlatformIcon(platform: string) {
|
||||
switch (platform) {
|
||||
case "Discord Android":
|
||||
case "Discord iOS":
|
||||
case "Discord Client":
|
||||
return DiscordIcon;
|
||||
case "Android Chrome":
|
||||
case "Chrome iOS":
|
||||
case "Chrome":
|
||||
return ChromeIcon;
|
||||
case "Edge":
|
||||
return EdgeIcon;
|
||||
case "Firefox":
|
||||
return FirefoxIcon;
|
||||
case "Internet Explorer":
|
||||
return IEIcon;
|
||||
case "Opera Mini":
|
||||
case "Opera":
|
||||
return OperaIcon;
|
||||
case "Mobile Safari":
|
||||
case "Safari":
|
||||
return SafariIcon;
|
||||
case "BlackBerry":
|
||||
case "Facebook Mobile":
|
||||
case "Android Mobile":
|
||||
return MobileIcon;
|
||||
default:
|
||||
return UnknownIcon;
|
||||
}
|
||||
}
|
|
@ -6,17 +6,18 @@
|
|||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
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 type { HTMLAttributes, ReactElement } from "react";
|
||||
|
||||
type SettingsEntry = { section: string, label: string; };
|
||||
|
||||
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({
|
||||
disableFade: {
|
||||
|
@ -118,18 +119,25 @@ export default definePlugin({
|
|||
{ // Settings cog context menu
|
||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||
replacement: {
|
||||
match: /\(0,\i.default\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
||||
match: /\(0,\i.useDefaultUserSettingsSections\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
||||
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) {
|
||||
return (
|
||||
<ErrorBoundary fallback={() => props.children as any}>
|
||||
<Layer {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
if (!FocusLock || !ComponentDispatch || !Classes) {
|
||||
new Logger("BetterSettings").error("Failed to find some components");
|
||||
return props.children;
|
||||
}
|
||||
|
||||
return <Layer {...props} />;
|
||||
},
|
||||
|
||||
wrapMenu(list: SettingsEntry[]) {
|
||||
|
|
|
@ -24,22 +24,20 @@ import { closeAllModals } from "@utils/modal";
|
|||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { maybePromptToUpdate } from "@utils/updater";
|
||||
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 { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => {
|
||||
const modules = findBulk(
|
||||
|
||||
const { ModalStack, DraftManager, closeExpressionPicker } = proxyLazyWebpack(() => {
|
||||
const [ModalStack, DraftManager, ExpressionManager] = findBulk(
|
||||
filters.byProps("pushLazy", "popAll"),
|
||||
filters.byProps("clearDraft", "saveDraft"),
|
||||
filters.byProps("DraftType"),
|
||||
filters.byProps("closeExpressionPicker", "openExpressionPicker"),
|
||||
);
|
||||
filters.byProps("closeExpressionPicker", "openExpressionPicker"),);
|
||||
|
||||
return {
|
||||
ModalStack: modules[0],
|
||||
DraftManager: modules[1],
|
||||
DraftType: modules[2]?.DraftType,
|
||||
closeExpressionPicker: modules[3]?.closeExpressionPicker,
|
||||
ModalStack,
|
||||
DraftManager,
|
||||
closeExpressionPicker: ExpressionManager?.closeExpressionPicker,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -104,7 +102,7 @@ export default definePlugin({
|
|||
|
||||
shouldAttemptRecover = false;
|
||||
// This is enough to avoid a crash loop
|
||||
setTimeout(() => shouldAttemptRecover = true, 500);
|
||||
setTimeout(() => shouldAttemptRecover = true, 1000);
|
||||
} catch { }
|
||||
|
||||
try {
|
||||
|
@ -137,8 +135,11 @@ export default definePlugin({
|
|||
try {
|
||||
const channelId = SelectedChannelStore.getChannelId();
|
||||
|
||||
DraftManager.clearDraft(channelId, DraftType.ChannelMessage);
|
||||
DraftManager.clearDraft(channelId, DraftType.FirstThreadMessage);
|
||||
for (const key in DraftType) {
|
||||
if (!Number.isNaN(Number(key))) continue;
|
||||
|
||||
DraftManager.clearDraft(channelId, DraftType[key]);
|
||||
}
|
||||
} catch (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,17 +17,20 @@
|
|||
*/
|
||||
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Link } from "@components/Link";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { isTruthy } from "@utils/guards";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
|
||||
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, StatusSettingsStores, UserStore } from "@webpack/common";
|
||||
|
||||
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
|
||||
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
|
||||
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
|
||||
const Colors = findByPropsLazy("profileColors");
|
||||
|
||||
async function getApplicationAsset(key: string): Promise<string> {
|
||||
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
|
||||
|
@ -386,15 +389,36 @@ async function setRpc(disable?: boolean) {
|
|||
export default definePlugin({
|
||||
name: "CustomRPC",
|
||||
description: "Allows you to set a custom rich presence.",
|
||||
authors: [Devs.captain, Devs.AutumnVN],
|
||||
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
|
||||
start: setRpc,
|
||||
stop: () => setRpc(true),
|
||||
settings,
|
||||
|
||||
settingsAboutComponent: () => {
|
||||
const activity = useAwaiter(createActivity);
|
||||
const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
|
||||
const { profileThemeStyle } = useProfileThemeStyle({});
|
||||
|
||||
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>
|
||||
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
|
||||
get the application ID.
|
||||
|
@ -405,8 +429,10 @@ export default definePlugin({
|
|||
<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".
|
||||
</Forms.FormText>
|
||||
<Forms.FormDivider />
|
||||
<div style={{ width: "284px" }} className={Colors.profileColors}>
|
||||
|
||||
<Forms.FormDivider className={Margins.top8} />
|
||||
|
||||
<div style={{ width: "284px", ...profileThemeStyle }}>
|
||||
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
|
||||
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
|
||||
application={{ id: settings.store.appID }}
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin from "@utils/types";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Tooltip } from "@webpack/common";
|
||||
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})/;
|
||||
|
||||
async function embedDidMount(this: Component<Props>) {
|
||||
try {
|
||||
const { embed } = this.props;
|
||||
const { replaceElements } = settings.store;
|
||||
|
||||
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
|
||||
|
||||
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
|
||||
|
@ -58,12 +67,12 @@ async function embedDidMount(this: Component<Props>) {
|
|||
enabled: true
|
||||
};
|
||||
|
||||
if (hasTitle) {
|
||||
if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
|
||||
embed.dearrow.oldTitle = embed.rawTitle;
|
||||
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
|
||||
}
|
||||
|
||||
if (hasThumb) {
|
||||
if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
|
||||
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||
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({
|
||||
name: "Dearrow",
|
||||
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
|
||||
authors: [Devs.Ven],
|
||||
settings,
|
||||
|
||||
embedDidMount,
|
||||
renderButton(component: Component<Props>) {
|
||||
|
@ -154,7 +183,8 @@ export default definePlugin({
|
|||
// add dearrow button
|
||||
{
|
||||
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 definePlugin from "@utils/types";
|
||||
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";
|
||||
|
||||
const StickersStore = findStoreLazy("StickersStore");
|
||||
|
@ -54,9 +54,9 @@ const StickerExt = [, "png", "png", "json", "gif"] as const;
|
|||
|
||||
function getUrl(data: Data) {
|
||||
if (data.t === "Emoji")
|
||||
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}`;
|
||||
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}?size=4096&lossless=true`;
|
||||
|
||||
return `${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.${StickerExt[data.format_type]}`;
|
||||
return `${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.${StickerExt[data.format_type]}?size=4096&lossless=true`;
|
||||
}
|
||||
|
||||
async function fetchSticker(id: string) {
|
||||
|
@ -64,7 +64,7 @@ async function fetchSticker(id: string) {
|
|||
if (cached) return cached;
|
||||
|
||||
const { body } = await RestAPI.get({
|
||||
url: `/stickers/${id}`
|
||||
url: Constants.Endpoints.STICKER(id)
|
||||
});
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
|
@ -83,7 +83,7 @@ async function cloneSticker(guildId: string, sticker: Sticker) {
|
|||
data.append("file", await fetchBlob(getUrl(sticker)));
|
||||
|
||||
const { body } = await RestAPI.post({
|
||||
url: `/guilds/${guildId}/stickers`,
|
||||
url: Constants.Endpoints.GUILD_STICKER_PACKS(guildId),
|
||||
body: data,
|
||||
});
|
||||
|
||||
|
@ -130,7 +130,8 @@ function getGuildCandidates(data: Data) {
|
|||
|
||||
let count = 0;
|
||||
for (const emoji of emojis)
|
||||
if (emoji.animated === isAnimated) count++;
|
||||
if (emoji.animated === isAnimated && !emoji.managed)
|
||||
count++;
|
||||
return count < emojiSlots;
|
||||
}).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
@ -321,8 +322,9 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
|
|||
switch (favoriteableType) {
|
||||
case "emoji":
|
||||
const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
|
||||
if (!match) return;
|
||||
const name = match[1] ?? "FakeNitroEmoji";
|
||||
const reaction = props.message.reactions.find(reaction => reaction.emoji.id === favoriteableId);
|
||||
if (!match && !reaction) return;
|
||||
const name = (match && match[1]) ?? reaction?.emoji.name ?? "FakeNitroEmoji";
|
||||
|
||||
return buildMenuItem("Emoji", () => ({
|
||||
id: favoriteableId,
|
||||
|
|
|
@ -20,10 +20,12 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
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");
|
||||
|
||||
|
@ -68,8 +70,8 @@ export default definePlugin({
|
|||
predicate: () => settings.store.enableIsStaff,
|
||||
replacement: [
|
||||
{
|
||||
match: /=>*?(\i)\.hasFlag\((\i\.\i)\.STAFF\)}/,
|
||||
replace: (_, user, flags) => `=>Vencord.Webpack.Common.UserStore.getCurrentUser()?.id===${user}.id||${user}.hasFlag(${flags}.STAFF)}`
|
||||
match: /(?<=>)(\i)\.hasFlag\((\i\.\i)\.STAFF\)(?=})/,
|
||||
replace: (_, user, flags) => `$self.isStaff(${user},${flags})`
|
||||
},
|
||||
{
|
||||
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: () => {
|
||||
const isMacOS = navigator.platform.includes("Mac");
|
||||
const modKey = isMacOS ? "cmd" : "ctrl";
|
||||
|
|
|
@ -24,12 +24,12 @@ import { getCurrentGuild } from "@utils/discord";
|
|||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
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 { Message } from "discord-types/general";
|
||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
|
||||
const DRAFT_TYPE = 0;
|
||||
const StickerStore = findStoreLazy("StickersStore") as {
|
||||
getPremiumPacks(): StickerPack[];
|
||||
getAllGuildStickers(): Map<string, Sticker[]>;
|
||||
|
@ -38,6 +38,7 @@ const StickerStore = findStoreLazy("StickersStore") as {
|
|||
|
||||
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
||||
const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
|
||||
const RoleSubscriptionEmojiUtils = findByPropsLazy("isUnusableRoleSubscriptionEmoji");
|
||||
|
||||
function searchProtoClassField(localName: string, protoClass: any) {
|
||||
const field = protoClass?.fields?.find((field: any) => field.localName === localName);
|
||||
|
@ -110,7 +111,7 @@ const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
|
|||
|
||||
const settings = definePluginSettings({
|
||||
enableEmojiBypass: {
|
||||
description: "Allow sending fake emojis",
|
||||
description: "Allows sending fake emojis (also bypasses missing permission to use custom emojis)",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
|
@ -128,7 +129,7 @@ const settings = definePluginSettings({
|
|||
restartNeeded: true
|
||||
},
|
||||
enableStickerBypass: {
|
||||
description: "Allow sending fake stickers",
|
||||
description: "Allows sending fake stickers (also bypasses missing permission to use stickers)",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
|
@ -165,10 +166,13 @@ const settings = definePluginSettings({
|
|||
description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.",
|
||||
type: OptionType.STRING,
|
||||
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) {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
|
@ -186,7 +190,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
|
|||
export default definePlugin({
|
||||
name: "FakeNitro",
|
||||
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"],
|
||||
|
||||
settings,
|
||||
|
@ -396,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,
|
||||
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"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -412,7 +433,8 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
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;
|
||||
|
||||
|
@ -438,6 +460,9 @@ export default definePlugin({
|
|||
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
new Logger("FakeNitro").error(err);
|
||||
}
|
||||
},
|
||||
|
||||
handleGradientThemeSelect(backgroundGradientPresetId: number | undefined, theme: number, original: () => void) {
|
||||
|
@ -781,7 +806,20 @@ export default definePlugin({
|
|||
gif.finish();
|
||||
|
||||
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) {
|
||||
if (e.require_colons === false) return true;
|
||||
if (e.available === false) return false;
|
||||
|
||||
const isUnusableRoleSubEmoji = RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
|
||||
if (isUnusableRoleSubEmoji(e, this.guildId)) return false;
|
||||
|
||||
if (this.canUseEmotes)
|
||||
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
|
||||
else
|
||||
return !e.animated && e.guildId === this.guildId;
|
||||
},
|
||||
|
||||
start() {
|
||||
|
@ -882,18 +920,14 @@ export default definePlugin({
|
|||
}
|
||||
|
||||
if (s.enableEmojiBypass) {
|
||||
const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId);
|
||||
|
||||
for (const emoji of messageObj.validNonShortcutEmojis) {
|
||||
if (!emoji.require_colons) continue;
|
||||
if (emoji.available !== false && canUseEmotes) continue;
|
||||
if (emoji.guildId === guildId && !emoji.animated) continue;
|
||||
if (this.canUseEmote(emoji, channelId)) continue;
|
||||
|
||||
hasBypass = true;
|
||||
|
||||
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("name", emoji.name);
|
||||
|
||||
|
@ -917,22 +951,16 @@ export default definePlugin({
|
|||
this.preEdit = addPreEditListener(async (channelId, __, messageObj) => {
|
||||
if (!s.enableEmojiBypass) return;
|
||||
|
||||
const { guildId } = this;
|
||||
|
||||
let hasBypass = false;
|
||||
|
||||
const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId);
|
||||
|
||||
messageObj.content = messageObj.content.replace(/(?<!\\)<a?:(?:\w+):(\d+)>/ig, (emojiStr, emojiId, offset, origStr) => {
|
||||
const emoji = EmojiStore.getCustomEmojiById(emojiId);
|
||||
if (emoji == null) return emojiStr;
|
||||
if (!emoji.require_colons) return emojiStr;
|
||||
if (emoji.available !== false && canUseEmotes) return emojiStr;
|
||||
if (emoji.guildId === guildId && !emoji.animated) return emojiStr;
|
||||
if (this.canUseEmote(emoji, channelId)) return emojiStr;
|
||||
|
||||
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("name", emoji.name);
|
||||
|
||||
|
|
|
@ -92,8 +92,9 @@ export default definePlugin({
|
|||
match: /(?<=getUserProfile\(\i\){return )(\i\[\i\])/,
|
||||
replace: "$self.colorDecodeHook($1)"
|
||||
}
|
||||
}, {
|
||||
find: ".USER_SETTINGS_PROFILE_THEME_ACCENT",
|
||||
},
|
||||
{
|
||||
find: ".USER_SETTINGS_RESET_PROFILE_THEME",
|
||||
replacement: {
|
||||
match: /RESET_PROFILE_THEME}\)(?<=color:(\i),.{0,500}?color:(\i),.{0,500}?)/,
|
||||
replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})"
|
||||
|
|
|
@ -83,6 +83,6 @@ export default definePlugin({
|
|||
if (!aIsFavorite && bIsFavorite) return 1;
|
||||
|
||||
return 0;
|
||||
}).slice(0, query.results.emojis.sliceTo ?? 10);
|
||||
}).slice(0, query.results.emojis.sliceTo ?? Infinity);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption,
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { RestAPI, UserStore } from "@webpack/common";
|
||||
import { Constants, RestAPI, UserStore } from "@webpack/common";
|
||||
|
||||
const FriendInvites = findByPropsLazy("createFriendInvite");
|
||||
const { uuid4 } = findByPropsLazy("uuid4");
|
||||
|
@ -58,7 +58,7 @@ export default definePlugin({
|
|||
if (uses === 1) {
|
||||
const random = uuid4();
|
||||
const { body: { invite_suggestions } } = await RestAPI.post({
|
||||
url: "/friend-finder/find-friends",
|
||||
url: Constants.Endpoints.FRIEND_FINDER,
|
||||
body: {
|
||||
modified_contacts: {
|
||||
[random]: [1, "", ""]
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentChannel } from "@utils/discord";
|
||||
import definePlugin from "@utils/types";
|
||||
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("memberSinceContainer");
|
||||
const container = findByPropsLazy("memberSinceWrapper");
|
||||
const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate");
|
||||
const clydeMoreInfo = findByPropsLazy("clydeMoreInfo");
|
||||
const locale = findByPropsLazy("getLocale");
|
||||
|
@ -48,7 +48,19 @@ export default definePlugin({
|
|||
Friends Since
|
||||
</Heading>
|
||||
|
||||
<div className={container.memberSinceContainer}>
|
||||
<div className={container.memberSinceWrapper}>
|
||||
{!!getCurrentChannel()?.guild_id && (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="var(--interactive-normal)"
|
||||
>
|
||||
<path d="M13 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" />
|
||||
<path d="M3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z" />
|
||||
</svg>
|
||||
)}
|
||||
<Text variant="text-sm/normal" className={clydeMoreInfo.body}>
|
||||
{getCreatedAtDate(friendsSince, locale.getLocale())}
|
||||
</Text>
|
||||
|
|
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 ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
|
||||
|
||||
import { ELEMENT_ID } from "../constants";
|
||||
|
@ -36,7 +37,7 @@ export interface MagnifierProps {
|
|||
|
||||
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 [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });
|
||||
|
@ -199,4 +200,4 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}, { noop: true });
|
||||
|
|
7
src/plugins/implicitRelationships/README.md
Normal file
7
src/plugins/implicitRelationships/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# ImplicitRelationships
|
||||
|
||||
Shows your implicit relationships in the Friends tab.
|
||||
|
||||
Implicit relationships on Discord are people with whom you've frecently interacted and share a mutual server; even though Discord thinks you should be friends with them, you haven't added them as friends.
|
||||
|
||||

|
187
src/plugins/implicitRelationships/index.ts
Normal file
187
src/plugins/implicitRelationships/index.ts
Normal file
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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 { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common";
|
||||
import { Settings } from "Vencord";
|
||||
|
||||
const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore");
|
||||
const { FriendsSections } = findByPropsLazy("FriendsSections");
|
||||
|
||||
interface UserAffinity {
|
||||
user_id: string;
|
||||
affinity: number;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "ImplicitRelationships",
|
||||
description: "Shows your implicit relationships in the Friends tab.",
|
||||
authors: [Devs.Dolfies],
|
||||
patches: [
|
||||
// Counts header
|
||||
{
|
||||
find: ".FRIENDS_ALL_HEADER",
|
||||
replacement: {
|
||||
match: /toString\(\)\}\);case (\i\.\i)\.BLOCKED/,
|
||||
replace: 'toString()});case $1.IMPLICIT:return "Implicit — "+arguments[1];case $1.BLOCKED'
|
||||
},
|
||||
},
|
||||
// No friends page
|
||||
{
|
||||
find: "FriendsEmptyState: Invalid empty state",
|
||||
replacement: {
|
||||
match: /case (\i\.\i)\.ONLINE:(?=return (\i)\.SECTION_ONLINE)/,
|
||||
replace: "case $1.ONLINE:case $1.IMPLICIT:"
|
||||
},
|
||||
},
|
||||
// Sections header
|
||||
{
|
||||
find: ".FRIENDS_SECTION_ONLINE",
|
||||
replacement: {
|
||||
match: /(\(0,\i\.jsx\)\(\i\.TabBar\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.Messages\.BLOCKED\}\)/,
|
||||
replace: "$1.IMPLICIT,className:$2.item,children:\"Implicit\"}),$&"
|
||||
},
|
||||
},
|
||||
// Sections content
|
||||
{
|
||||
find: '"FriendsStore"',
|
||||
replacement: {
|
||||
match: /(?<=case (\i\.\i)\.BLOCKED:return (\i)\.type===\i\.\i\.BLOCKED)/,
|
||||
replace: ";case $1.IMPLICIT:return $2.type===5"
|
||||
},
|
||||
},
|
||||
// Piggyback relationship fetch
|
||||
{
|
||||
find: ".fetchRelationships()",
|
||||
replacement: {
|
||||
match: /(\i\.\i)\.fetchRelationships\(\)/,
|
||||
// This relationship fetch is actually completely useless, but whatevs
|
||||
replace: "$1.fetchRelationships(),$self.fetchImplicitRelationships()"
|
||||
},
|
||||
},
|
||||
// Modify sort -- thanks megu for the patch (from sortFriendRequests)
|
||||
{
|
||||
find: "getRelationshipCounts(){",
|
||||
replacement: {
|
||||
predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity,
|
||||
match: /\}\)\.sortBy\((.+?)\)\.value\(\)/,
|
||||
replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()"
|
||||
}
|
||||
},
|
||||
|
||||
// Add support for the nonce parameter to Discord's shitcode
|
||||
{
|
||||
find: ".REQUEST_GUILD_MEMBERS",
|
||||
replacement: {
|
||||
match: /\.send\(8,{/,
|
||||
replace: "$&nonce:arguments[1].nonce,"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "GUILD_MEMBERS_REQUEST:",
|
||||
replacement: {
|
||||
match: /presences:!!(\i)\.presences/,
|
||||
replace: "$&,nonce:$1.nonce"
|
||||
},
|
||||
},
|
||||
{
|
||||
find: ".not_found",
|
||||
replacement: {
|
||||
match: /notFound:(\i)\.not_found/,
|
||||
replace: "$&,nonce:$1.nonce"
|
||||
},
|
||||
}
|
||||
],
|
||||
settings: definePluginSettings(
|
||||
{
|
||||
sortByAffinity: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Whether to sort implicit relationships by their affinity to you.",
|
||||
restartNeeded: true
|
||||
},
|
||||
}
|
||||
),
|
||||
|
||||
wrapSort(comparator: Function, row: any) {
|
||||
return row.type === 5
|
||||
? -UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0
|
||||
: comparator(row);
|
||||
},
|
||||
|
||||
async fetchImplicitRelationships() {
|
||||
// Implicit relationships are defined as users that you:
|
||||
// 1. Have an affinity for
|
||||
// 2. Do not have a relationship with // TODO: Check how this works with pending/blocked relationships
|
||||
// 3. Have a mutual guild with
|
||||
const userAffinities: Set<string> = UserAffinitiesStore.getUserAffinitiesUserIds();
|
||||
const nonFriendAffinities = Array.from(userAffinities).filter(
|
||||
id => !RelationshipStore.getRelationshipType(id)
|
||||
);
|
||||
|
||||
// I would love to just check user cache here (falling back to the gateway of course)
|
||||
// However, users in user cache may just be there because they share a DM or group DM with you
|
||||
// So there's no guarantee that a user being in user cache means they have a mutual with you
|
||||
// To get around this, we request users we have DMs with, and ignore them below if we don't get them back
|
||||
const dmUserIds = new Set(
|
||||
Object.values(ChannelStore.getSortedPrivateChannels()).flatMap(c => c.recipients)
|
||||
);
|
||||
const toRequest = nonFriendAffinities.filter(id => !UserStore.getUser(id) || dmUserIds.has(id));
|
||||
const allGuildIds = Object.keys(GuildStore.getGuilds());
|
||||
const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());
|
||||
let count = allGuildIds.length * Math.ceil(toRequest.length / 100);
|
||||
|
||||
// OP 8 Request Guild Members allows 100 user IDs at a time
|
||||
const ignore = new Set(toRequest);
|
||||
const relationships = RelationshipStore.getRelationships();
|
||||
const callback = ({ chunks }) => {
|
||||
for (const chunk of chunks) {
|
||||
const { nonce, members } = chunk;
|
||||
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);
|
||||
RelationshipStore.emitChange();
|
||||
if (--count === 0) {
|
||||
// @ts-ignore
|
||||
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
|
||||
for (let i = 0; i < toRequest.length; i += 100) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "GUILD_MEMBERS_REQUEST",
|
||||
guildIds: allGuildIds,
|
||||
userIds: toRequest.slice(i, i + 100),
|
||||
nonce: sentNonce,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
FriendsSections.IMPLICIT = "IMPLICIT";
|
||||
}
|
||||
});
|
|
@ -34,6 +34,10 @@ export const PMLogger = logger;
|
|||
export const plugins = Plugins;
|
||||
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;
|
||||
|
||||
export function isPluginEnabled(p: string) {
|
||||
|
@ -119,6 +123,37 @@ export function startDependenciesRecursive(p: Plugin) {
|
|||
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) {
|
||||
const { name, commands, flux, contextMenus } = p;
|
||||
|
||||
|
@ -138,7 +173,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
}
|
||||
|
||||
if (commands?.length) {
|
||||
logger.info("Registering commands of plugin", name);
|
||||
logger.debug("Registering commands of plugin", name);
|
||||
for (const cmd of commands) {
|
||||
try {
|
||||
registerCommand(cmd, name);
|
||||
|
@ -149,13 +184,13 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
}
|
||||
}
|
||||
|
||||
if (flux) {
|
||||
for (const event in flux) {
|
||||
FluxDispatcher.subscribe(event as FluxEvents, flux[event]);
|
||||
}
|
||||
if (enabledPluginsSubscribedFlux) {
|
||||
subscribePluginFluxEvents(p, FluxDispatcher);
|
||||
}
|
||||
|
||||
|
||||
if (contextMenus) {
|
||||
logger.debug("Adding context menus patches of plugin", name);
|
||||
for (const navId in contextMenus) {
|
||||
addContextMenuPatch(navId, contextMenus[navId]);
|
||||
}
|
||||
|
@ -182,7 +217,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
|||
}
|
||||
|
||||
if (commands?.length) {
|
||||
logger.info("Unregistering commands of plugin", name);
|
||||
logger.debug("Unregistering commands of plugin", name);
|
||||
for (const cmd of commands) {
|
||||
try {
|
||||
unregisterCommand(cmd.name);
|
||||
|
@ -193,13 +228,10 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
|||
}
|
||||
}
|
||||
|
||||
if (flux) {
|
||||
for (const event in flux) {
|
||||
FluxDispatcher.unsubscribe(event as FluxEvents, flux[event]);
|
||||
}
|
||||
}
|
||||
unsubscribePluginFluxEvents(p, FluxDispatcher);
|
||||
|
||||
if (contextMenus) {
|
||||
logger.debug("Removing context menus patches of plugin", name);
|
||||
for (const navId in contextMenus) {
|
||||
removeContextMenuPatch(navId, contextMenus[navId]);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { getStegCloak } from "@utils/dependencies";
|
||||
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 { buildDecModal } from "./components/DecryptionModal";
|
||||
|
@ -153,7 +153,7 @@ export default definePlugin({
|
|||
// Gets the Embed of a Link
|
||||
async getEmbed(url: URL): Promise<Object | {}> {
|
||||
const { body } = await RestAPI.post({
|
||||
url: "/unfurler/embed-urls",
|
||||
url: Constants.Endpoints.UNFURL_EMBED_URLS,
|
||||
body: {
|
||||
urls: [url]
|
||||
}
|
||||
|
|
|
@ -77,7 +77,8 @@ const enum NameFormat {
|
|||
ArtistFirst = "artist-first",
|
||||
SongFirst = "song-first",
|
||||
ArtistOnly = "artist",
|
||||
SongOnly = "song"
|
||||
SongOnly = "song",
|
||||
AlbumName = "album"
|
||||
}
|
||||
|
||||
const applicationId = "1108588077900898414";
|
||||
|
@ -147,6 +148,10 @@ const settings = definePluginSettings({
|
|||
{
|
||||
label: "Use song name only",
|
||||
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;
|
||||
case NameFormat.SongOnly:
|
||||
return trackData.name;
|
||||
case NameFormat.AlbumName:
|
||||
return trackData.album || settings.store.statusName;
|
||||
default:
|
||||
return settings.store.statusName;
|
||||
}
|
||||
|
|
|
@ -17,17 +17,19 @@
|
|||
*/
|
||||
|
||||
import { addClickListener, removeClickListener } from "@api/MessageEvents";
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore } from "@webpack/common";
|
||||
|
||||
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
||||
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
||||
|
||||
let isDeletePressed = false;
|
||||
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
||||
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
|
||||
|
||||
|
||||
const settings = definePluginSettings({
|
||||
enableDeleteOnClick: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
@ -60,9 +62,6 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
start() {
|
||||
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
||||
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
||||
|
||||
document.addEventListener("keydown", keydown);
|
||||
document.addEventListener("keyup", keyup);
|
||||
|
||||
|
@ -85,11 +84,17 @@ export default definePlugin({
|
|||
const EPHEMERAL = 64;
|
||||
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({
|
||||
type: "CREATE_PENDING_REPLY",
|
||||
channel,
|
||||
message: msg,
|
||||
shouldMention: !Settings.plugins.NoReplyMention.enabled,
|
||||
shouldMention,
|
||||
showMentionToggle: channel.guild_id !== null
|
||||
});
|
||||
}
|
||||
|
|
31
src/plugins/messageLatency/README.md
Normal file
31
src/plugins/messageLatency/README.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
# MessageLatency
|
||||
|
||||
Displays an indicator for messages that took ≥n seconds to send.
|
||||
|
||||
> **NOTE**
|
||||
>
|
||||
> - This plugin only applies to messages received after opening the channel
|
||||
> - False positives can exist if the user's system clock has drifted.
|
||||
> - Grouped messages only display latency of the first message
|
||||
|
||||
## Demo
|
||||
|
||||
### Chat View
|
||||
|
||||

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

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

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

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

|
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>;
|
||||
}
|
||||
});
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addAccessory } from "@api/MessageAccessories";
|
||||
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants.js";
|
||||
|
@ -27,6 +27,7 @@ import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
|||
import {
|
||||
Button,
|
||||
ChannelStore,
|
||||
Constants,
|
||||
FluxDispatcher,
|
||||
GuildStore,
|
||||
IconUtils,
|
||||
|
@ -132,7 +133,7 @@ async function fetchMessage(channelID: string, messageID: string) {
|
|||
messageCache.set(messageID, { fetched: false });
|
||||
|
||||
const res = await RestAPI.get({
|
||||
url: `/channels/${channelID}/messages`,
|
||||
url: Constants.Endpoints.MESSAGES(channelID),
|
||||
query: {
|
||||
limit: 1,
|
||||
around: messageID
|
||||
|
@ -389,4 +390,8 @@ export default definePlugin({
|
|||
);
|
||||
}, 4 /* just above rich embeds */);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeAccessory("messageLinkEmbed");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
.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 */
|
||||
.messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) {
|
||||
color: #f04747 !important;
|
||||
color: var(--status-danger, #f04747) !important;
|
||||
}
|
||||
|
||||
/* Bot "thinking" text highlighting */
|
||||
.messagelogger-deleted [class*="colorStandard"] {
|
||||
color: #f04747 !important;
|
||||
color: var(--status-danger, #f04747) !important;
|
||||
}
|
||||
|
||||
/* Embed highlighting */
|
||||
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
|
||||
color: #f04747 !important;
|
||||
color: var(--status-danger, #f04747) !important;
|
||||
}
|
||||
|
||||
.messagelogger-deleted a {
|
||||
color: #be3535 !important;
|
||||
color: var(--red-460, #be3535) !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
|
@ -217,7 +217,9 @@ export default definePlugin({
|
|||
ignoreChannels.includes(message.channel_id) ||
|
||||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
|
||||
(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
|
||||
|
@ -337,12 +339,12 @@ export default definePlugin({
|
|||
{
|
||||
// Attachment renderer
|
||||
// Module 96063
|
||||
find: ".removeAttachmentHoverButton",
|
||||
find: ".removeMosaicItemHoverButton",
|
||||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
match: /(className:\i,attachment:\i),/,
|
||||
replace: "$1,attachment: {deleted},"
|
||||
match: /(className:\i,item:\i),/,
|
||||
replace: "$1,item: deleted,"
|
||||
},
|
||||
{
|
||||
match: /\[\i\.obscured\]:.+?,/,
|
||||
|
|
|
@ -50,6 +50,7 @@ interface TagSettings {
|
|||
MODERATOR_STAFF: TagSetting,
|
||||
MODERATOR: TagSetting,
|
||||
VOICE_MODERATOR: TagSetting,
|
||||
TRIAL_MODERATOR: TagSetting,
|
||||
[k: string]: TagSetting;
|
||||
}
|
||||
|
||||
|
@ -93,6 +94,11 @@ const tags: Tag[] = [
|
|||
displayName: "VC Mod",
|
||||
description: "Can manage voice chats",
|
||||
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
|
||||
}, {
|
||||
name: "CHAT_MODERATOR",
|
||||
displayName: "Chat Mod",
|
||||
description: "Can timeout people",
|
||||
permissions: ["MODERATE_MEMBERS"]
|
||||
}
|
||||
];
|
||||
const defaultSettings = Object.fromEntries(
|
||||
|
@ -198,7 +204,7 @@ export default definePlugin({
|
|||
replacement: [
|
||||
// make the tag show the right text
|
||||
{
|
||||
match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.BOT_TAG_BOT/,
|
||||
match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.APP_TAG/,
|
||||
replace: (_, origSwitch, variant, tags, displayedText, strings) =>
|
||||
`${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}`
|
||||
},
|
||||
|
@ -263,34 +269,14 @@ export default definePlugin({
|
|||
],
|
||||
|
||||
start() {
|
||||
if (settings.store.tagSettings) return;
|
||||
// @ts-ignore
|
||||
if (!settings.store.visibility_WEBHOOK) settings.store.tagSettings = defaultSettings;
|
||||
else {
|
||||
const newSettings = { ...defaultSettings };
|
||||
Object.entries(Vencord.PlainSettings.plugins.MoreUserTags).forEach(([name, value]) => {
|
||||
const [setting, tag] = name.split("_");
|
||||
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];
|
||||
});
|
||||
}
|
||||
settings.store.tagSettings ??= defaultSettings;
|
||||
|
||||
// newly added field might be missing from old users
|
||||
settings.store.tagSettings.CHAT_MODERATOR ??= {
|
||||
text: "Chat Mod",
|
||||
showInChat: true,
|
||||
showInNotChat: true
|
||||
};
|
||||
},
|
||||
|
||||
getPermissions(user: User, channel: Channel): string[] {
|
||||
|
@ -322,18 +308,18 @@ export default definePlugin({
|
|||
isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]),
|
||||
|
||||
getTagText(passedTagName: string, strings: Record<string, string>) {
|
||||
if (!passedTagName) return strings.BOT_TAG_BOT;
|
||||
if (!passedTagName) return strings.APP_TAG;
|
||||
const [tagName, variant] = passedTagName.split("-");
|
||||
const tag = tags.find(({ name }) => tagName === name);
|
||||
if (!tag) return strings.BOT_TAG_BOT;
|
||||
if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return strings.BOT_TAG_BOT;
|
||||
if (!tag) return strings.APP_TAG;
|
||||
if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return strings.APP_TAG;
|
||||
|
||||
const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName;
|
||||
switch (variant) {
|
||||
case "OP":
|
||||
return `${strings.BOT_TAG_FORUM_ORIGINAL_POSTER} • ${tagText}`;
|
||||
case "BOT":
|
||||
return `${strings.BOT_TAG_BOT} • ${tagText}`;
|
||||
return `${strings.APP_TAG} • ${tagText}`;
|
||||
default:
|
||||
return tagText;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { isNonNullish } from "@utils/guards";
|
||||
import definePlugin from "@utils/types";
|
||||
|
@ -45,7 +46,7 @@ export default definePlugin({
|
|||
|
||||
patches: [
|
||||
{
|
||||
find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded
|
||||
find: ".Messages.MUTUAL_GUILDS_WITH_END_COUNT", // Note: the module is lazy-loaded
|
||||
replacement: {
|
||||
match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/,
|
||||
replace: '(arguments[0].user.bot||arguments[0].isCurrentUser)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),'
|
||||
|
@ -55,12 +56,12 @@ export default definePlugin({
|
|||
find: ".UserProfileSections.USER_INFO_CONNECTIONS:",
|
||||
replacement: {
|
||||
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 => (
|
||||
<Clickable
|
||||
className={ProfileListClasses.listRow}
|
||||
|
@ -99,5 +100,5 @@ export default definePlugin({
|
|||
}
|
||||
</ScrollerThin>
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
@ -31,6 +31,16 @@ const settings = definePluginSettings({
|
|||
type: OptionType.BOOLEAN,
|
||||
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: {
|
||||
description: "Suppress @everyone and @here",
|
||||
type: OptionType.BOOLEAN,
|
||||
|
@ -41,6 +51,16 @@ const settings = definePluginSettings({
|
|||
type: OptionType.BOOLEAN,
|
||||
default: true
|
||||
},
|
||||
highlights: {
|
||||
description: "Suppress Highlights automatically",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true
|
||||
},
|
||||
events: {
|
||||
description: "Mute New Events automatically",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true
|
||||
},
|
||||
showAllChannels: {
|
||||
description: "Show all channels automatically",
|
||||
type: OptionType.BOOLEAN,
|
||||
|
@ -53,7 +73,7 @@ export default definePlugin({
|
|||
name: "NewGuildSettings",
|
||||
description: "Automatically mute new servers and change various other settings upon joining",
|
||||
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: [
|
||||
{
|
||||
find: ",acceptInvite(",
|
||||
|
@ -78,8 +98,16 @@ export default definePlugin({
|
|||
{
|
||||
muted: settings.store.guild,
|
||||
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)) {
|
||||
toggleShowAllChannels(guildId);
|
||||
}
|
||||
|
|
|
@ -5,27 +5,15 @@
|
|||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
import style from "./styles.css?managed";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
inlineVideo: {
|
||||
description: "Play videos without carousel modal",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
mediaLayoutType: {
|
||||
description: "Choose media layout type",
|
||||
type: OptionType.SELECT,
|
||||
restartNeeded: true,
|
||||
options: [
|
||||
{ label: "STATIC, render loading image but image isn't resposive, no problem unless discord window width is too small", value: "STATIC", default: true },
|
||||
{ label: "RESPONSIVE, image is responsive but not render loading image, cause messages shift when loaded", value: "RESPONSIVE" },
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -39,15 +27,11 @@ export default definePlugin({
|
|||
|
||||
patches: [
|
||||
{
|
||||
find: ".oneByTwoLayoutThreeGrid",
|
||||
replacement: [{
|
||||
match: /mediaLayoutType:\i\.\i\.MOSAIC/,
|
||||
replace: "mediaLayoutType:$self.mediaLayoutType()",
|
||||
},
|
||||
{
|
||||
match: /null!==\(\i=\i\.get\(\i\)\)&&void 0!==\i\?\i:"INVALID"/,
|
||||
replace: '"INVALID"',
|
||||
}]
|
||||
find: "isGroupableMedia:function()",
|
||||
replacement: {
|
||||
match: /=>"IMAGE"===\i\|\|"VIDEO"===\i;/,
|
||||
replace: "=>false;"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "renderAttachments(",
|
||||
|
@ -57,24 +41,5 @@ export default definePlugin({
|
|||
replace: "$&$1.content_type?.startsWith('image/')&&"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "Messages.REMOVE_ATTACHMENT_TOOLTIP_TEXT",
|
||||
replacement: {
|
||||
match: /\i===\i\.\i\.MOSAIC/,
|
||||
replace: "true"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
mediaLayoutType() {
|
||||
return settings.store.mediaLayoutType;
|
||||
},
|
||||
|
||||
start() {
|
||||
enableStyle(style);
|
||||
},
|
||||
|
||||
stop() {
|
||||
disableStyle(style);
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[class^="nonMediaAttachmentsContainer_"] [class*="messageAttachment_"] {
|
||||
position: relative;
|
||||
}
|
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;
|
||||
}
|
||||
});
|
|
@ -49,7 +49,7 @@ export default definePlugin({
|
|||
replace: "$&if(!$self.isPrivateChannelRead(arguments[0]?.message))return;else "
|
||||
},
|
||||
{
|
||||
match: /sound:(\i\?\i:void 0,volume:\i,onClick)/,
|
||||
match: /sound:(\i\?\i:void 0,soundpack:\i,volume:\i,onClick)/,
|
||||
replace: "sound:!$self.isPrivateChannelRead(arguments[0]?.message)?undefined:$1"
|
||||
}]
|
||||
}],
|
||||
|
|
|
@ -26,6 +26,7 @@ const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
|||
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/;
|
||||
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
|
||||
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
|
||||
const TidalMatcher = /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/;
|
||||
|
||||
const settings = definePluginSettings({
|
||||
spotify: {
|
||||
|
@ -42,6 +43,11 @@ const settings = definePluginSettings({
|
|||
type: OptionType.BOOLEAN,
|
||||
description: "Open Epic Games links in the Epic Games Launcher",
|
||||
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({
|
||||
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],
|
||||
settings,
|
||||
|
||||
|
@ -127,6 +133,19 @@ export default definePlugin({
|
|||
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
|
||||
if (event?.defaultPrevented) {
|
||||
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.
|
||||
|
||||

|
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 { getUniqueUsername } from "@utils/discord";
|
||||
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 { settings } from "..";
|
||||
|
@ -112,7 +112,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
|||
<div
|
||||
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
|
||||
onContextMenu={e => {
|
||||
if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role)
|
||||
if (permission.type === PermissionType.Role)
|
||||
ContextMenuApi.openContextMenu(e, () => (
|
||||
<RoleContextMenu
|
||||
guild={guild}
|
||||
|
@ -120,6 +120,14 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
|||
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) && (
|
||||
|
@ -199,9 +207,18 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
|
|||
onClose={ContextMenuApi.closeContextMenu}
|
||||
aria-label="Role Options"
|
||||
>
|
||||
<Menu.MenuItem
|
||||
id="vc-copy-role-id"
|
||||
label={i18n.Messages.COPY_ID_ROLE}
|
||||
action={() => {
|
||||
Clipboard.copy(roleId);
|
||||
}}
|
||||
/>
|
||||
|
||||
{(settings.store as any).unsafeViewAsRole && (
|
||||
<Menu.MenuItem
|
||||
id="vc-pw-view-as-role"
|
||||
label="View As Role"
|
||||
label={i18n.Messages.VIEW_AS_ROLE}
|
||||
action={() => {
|
||||
const role = GuildStore.getRole(guild.id, roleId);
|
||||
if (!role) return;
|
||||
|
@ -218,6 +235,26 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
|
|
|
@ -21,10 +21,9 @@ import { Devs } from "@utils/constants";
|
|||
import { makeLazy } from "@utils/lazy";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { UploadHandler, UserUtils } from "@webpack/common";
|
||||
import { DraftType, UploadHandler, UploadManager, UserUtils } from "@webpack/common";
|
||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||
|
||||
const DRAFT_TYPE = 0;
|
||||
const DEFAULT_DELAY = 20;
|
||||
const DEFAULT_RESOLUTION = 128;
|
||||
const FRAMES = 10;
|
||||
|
@ -59,9 +58,12 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
|
|||
for (const opt of options) {
|
||||
switch (opt.name) {
|
||||
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.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;
|
||||
}
|
||||
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");
|
||||
} catch (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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -130,6 +134,7 @@ export default definePlugin({
|
|||
var url = await resolveImage(opts, cmdCtx, noServerPfp);
|
||||
if (!url) throw "No Image specified!";
|
||||
} catch (err) {
|
||||
UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
|
||||
sendBotMessage(cmdCtx.channel.id, {
|
||||
content: String(err),
|
||||
});
|
||||
|
@ -147,6 +152,8 @@ export default definePlugin({
|
|||
canvas.width = canvas.height = resolution;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
|
||||
|
||||
for (let i = 0; i < FRAMES; i++) {
|
||||
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" });
|
||||
// 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
|
||||
setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10);
|
||||
setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
@ -10,7 +10,7 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { React, Tooltip } from "@webpack/common";
|
||||
import { Tooltip } from "@webpack/common";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
loop: {
|
||||
|
@ -28,9 +28,9 @@ export default definePlugin({
|
|||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: ".nonMediaAttachment]",
|
||||
find: ".nonMediaMosaicItem]",
|
||||
replacement: {
|
||||
match: /\.nonMediaAttachment\]:!(\i).{0,10}children:\[(\S)/,
|
||||
match: /\.nonMediaMosaicItem\]:!(\i).{0,10}children:\[(\S)/,
|
||||
replace: "$&,$1&&$2&&$self.renderPiPButton(),"
|
||||
},
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Settings } from "@api/Settings";
|
|||
import { UserStore } from "@webpack/common";
|
||||
|
||||
import { DEFAULT_COLOR } from "./constants";
|
||||
import { forceUpdate } from "./index";
|
||||
import { forceUpdate, PinOrder, PrivateChannelSortStore, settings } from "./index";
|
||||
|
||||
export interface Category {
|
||||
id: string;
|
||||
|
@ -106,7 +106,12 @@ export function categoryLen() {
|
|||
}
|
||||
|
||||
export function getAllUncollapsedChannels() {
|
||||
return categories.filter(c => !c.collapsed).map(c => c.channels).flat();
|
||||
if (settings.store.pinOrder === PinOrder.LastMessage) {
|
||||
const sortedChannels = PrivateChannelSortStore.getPrivateChannelIds();
|
||||
return categories.filter(c => !c.collapsed).flatMap(c => sortedChannels.filter(channel => c.channels.includes(channel)));
|
||||
}
|
||||
|
||||
return categories.filter(c => !c.collapsed).flatMap(c => c.channels);
|
||||
}
|
||||
|
||||
export function getSections() {
|
||||
|
|
|
@ -29,7 +29,7 @@ interface ChannelComponentProps {
|
|||
|
||||
const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
|
||||
|
||||
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
|
||||
export const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
|
||||
|
||||
export let instance: any;
|
||||
export const forceUpdate = () => instance?.props?._forceUpdate?.();
|
||||
|
@ -83,7 +83,7 @@ export default definePlugin({
|
|||
// Rendering
|
||||
{
|
||||
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)=>{/,
|
||||
|
@ -236,7 +236,7 @@ export default definePlugin({
|
|||
const category = categories[categoryIndex - 1];
|
||||
if (!category) return false;
|
||||
|
||||
return category.collapsed && this.instance.props.selectedChannelId !== category.channels[channelIndex];
|
||||
return category.collapsed && this.instance.props.selectedChannelId !== this.getCategoryChannels(category)[channelIndex];
|
||||
},
|
||||
|
||||
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
|
||||
|
@ -320,9 +320,10 @@ export default definePlugin({
|
|||
</svg>
|
||||
</h2>
|
||||
);
|
||||
}),
|
||||
}, { noop: true }),
|
||||
|
||||
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
|
||||
return ErrorBoundary.wrap(() => {
|
||||
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
|
||||
|
||||
if (!channel || !category) return null;
|
||||
|
@ -336,9 +337,9 @@ export default definePlugin({
|
|||
{channel.id}
|
||||
</ChannelComponent>
|
||||
);
|
||||
}, { noop: true });
|
||||
},
|
||||
|
||||
|
||||
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
|
||||
const category = categories[sectionIndex - 1];
|
||||
if (!category) return { channel: null, category: null };
|
||||
|
|
|
@ -33,31 +33,30 @@ const PRONOUN_TOOLTIP_PATCH = {
|
|||
|
||||
export default definePlugin({
|
||||
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",
|
||||
patches: [
|
||||
// Add next to username (compact mode)
|
||||
{
|
||||
find: "showCommunicationDisabledStyles",
|
||||
replacement: {
|
||||
replacement: [
|
||||
// Add next to username (compact mode)
|
||||
{
|
||||
match: /("span",{id:\i,className:\i,children:\i}\))/,
|
||||
replace: "$1, $self.CompactPronounsChatComponentWrapper(arguments[0])"
|
||||
}
|
||||
},
|
||||
// Patch the chat timestamp element (normal mode)
|
||||
{
|
||||
find: "showCommunicationDisabledStyles",
|
||||
replacement: {
|
||||
match: /(?<=return\s*\(0,\i\.jsxs?\)\(.+!\i&&)(\(0,\i.jsxs?\)\(.+?\{.+?\}\))/,
|
||||
replace: "[$1, $self.PronounsChatComponentWrapper(arguments[0])]"
|
||||
}
|
||||
]
|
||||
},
|
||||
// Patch the profile popout username header to use our pronoun hook instead of Discord's pronouns
|
||||
{
|
||||
find: ".userTagNoNickname",
|
||||
find: ".pronouns,children",
|
||||
replacement: [
|
||||
{
|
||||
match: /{user:(\i),[^}]*,pronouns:(\i),[^}]*}=\i;/,
|
||||
match: /{user:(\i),[^}]*,pronouns:(\i),[^}]*}=\i.*?;(?=return)/,
|
||||
replace: "$&let vcPronounSource;[$2,vcPronounSource]=$self.useProfilePronouns($1.id);"
|
||||
},
|
||||
PRONOUN_TOOLTIP_PATCH
|
||||
|
|
|
@ -24,7 +24,7 @@ import { useAwaiter } from "@utils/react";
|
|||
import { UserProfileStore, UserStore } from "@webpack/common";
|
||||
|
||||
import { settings } from "./settings";
|
||||
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
|
||||
import { CachePronouns, PronounCode, PronounMapping, PronounsResponse } from "./types";
|
||||
|
||||
type PronounsWithSource = [string | null, string];
|
||||
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
|
||||
const cache: Record<string, PronounCode> = {};
|
||||
const cache: Record<string, CachePronouns> = {};
|
||||
// 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
|
||||
const bulkFetch = debounce(async () => {
|
||||
|
@ -50,7 +50,7 @@ const bulkFetch = debounce(async () => {
|
|||
const pronouns = await bulkFetchPronouns(ids);
|
||||
for (const id of ids) {
|
||||
// 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];
|
||||
}
|
||||
});
|
||||
|
@ -78,8 +78,8 @@ export function useFormattedPronouns(id: string, useGlobalProfile: boolean = fal
|
|||
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
|
||||
return [discordPronouns, "Discord"];
|
||||
|
||||
if (result && result !== "unspecified")
|
||||
return [formatPronouns(result), "PronounDB"];
|
||||
if (result && result !== PronounMapping.unspecified)
|
||||
return [result, "PronounDB"];
|
||||
|
||||
return [discordPronouns, "Discord"];
|
||||
}
|
||||
|
@ -98,8 +98,9 @@ const NewLineRe = /\n+/g;
|
|||
|
||||
// Gets the cached pronouns, if you're too impatient for a promise!
|
||||
export function getCachedPronouns(id: string): string | null {
|
||||
const cached = cache[id];
|
||||
if (cached && cached !== "unspecified") return cached;
|
||||
const cached = cache[id] ? extractPronouns(cache[id].sets) : undefined;
|
||||
|
||||
if (cached && cached !== PronounMapping.unspecified) return cached;
|
||||
|
||||
return cached || null;
|
||||
}
|
||||
|
@ -125,7 +126,7 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
|
|||
params.append("ids", ids.join(","));
|
||||
|
||||
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",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
|
@ -140,21 +141,24 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
|
|||
} catch (e) {
|
||||
// If the request errors, treat it as if no pronouns were found for all ids, and log it
|
||||
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);
|
||||
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; };
|
||||
// For capitalized pronouns, just return the mapping (it is by default capitalized)
|
||||
if (pronounsFormat === PronounsFormat.Capitalized) return PronounMapping[pronouns];
|
||||
// If it is set to lowercase and a special code (any, ask, avoid), then just return the capitalized text
|
||||
else if (
|
||||
pronounsFormat === PronounsFormat.Lowercase
|
||||
&& ["any", "ask", "avoid", "other"].includes(pronouns)
|
||||
) return PronounMapping[pronouns];
|
||||
// Otherwise (lowercase and not a special code), then convert the mapping to lowercase
|
||||
else return PronounMapping[pronouns].toLowerCase();
|
||||
|
||||
if (pronouns.length === 1) {
|
||||
// For capitalized pronouns or special codes (any, ask, avoid), we always return the normal (capitalized) string
|
||||
if (pronounsFormat === PronounsFormat.Capitalized || ["any", "ask", "avoid", "other", "unspecified"].includes(pronouns[0]))
|
||||
return PronounMapping[pronouns[0]];
|
||||
else return PronounMapping[pronouns[0]].toLowerCase();
|
||||
}
|
||||
const pronounString = pronouns.map(p => p[0].toUpperCase() + p.slice(1)).join("/");
|
||||
return pronounsFormat === PronounsFormat.Capitalized ? pronounString : pronounString.toLowerCase();
|
||||
}
|
||||
|
|
|
@ -26,31 +26,29 @@ export interface UserProfilePronounsProps {
|
|||
}
|
||||
|
||||
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 const PronounMapping = {
|
||||
hh: "He/Him",
|
||||
hi: "He/It",
|
||||
hs: "He/She",
|
||||
ht: "He/They",
|
||||
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",
|
||||
he: "He/Him",
|
||||
it: "It/Its",
|
||||
she: "She/Her",
|
||||
they: "They/Them",
|
||||
any: "Any pronouns",
|
||||
other: "Other pronouns",
|
||||
ask: "Ask me my pronouns",
|
||||
avoid: "Avoid pronouns, use my name",
|
||||
unspecified: "Unspecified"
|
||||
unspecified: "No pronouns specified.",
|
||||
} as const;
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import "./style.css";
|
||||
|
||||
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
|
||||
|
@ -64,7 +65,7 @@ export default definePlugin({
|
|||
authors: [Devs.kemo],
|
||||
dependencies: ["ServerListAPI"],
|
||||
|
||||
renderReadAllButton: () => <ReadAllButton />,
|
||||
renderReadAllButton: ErrorBoundary.wrap(ReadAllButton, { noop: true }),
|
||||
|
||||
start() {
|
||||
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.
|
||||
|
||||

|
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;
|
||||
}
|
|
@ -18,9 +18,68 @@
|
|||
|
||||
import { findGroupChildrenByChildId } from "@api/ContextMenu";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Menu } from "@webpack/common";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Button, Menu, Tooltip, useEffect, useState } from "@webpack/common";
|
||||
|
||||
const ChannelRowClasses = findByPropsLazy("modeConnected", "modeLocked", "icon");
|
||||
|
||||
let currentShouldViewServerHome = false;
|
||||
const shouldViewServerHomeStates = new Set<React.Dispatch<React.SetStateAction<boolean>>>();
|
||||
|
||||
function ViewServerHomeButton() {
|
||||
return (
|
||||
<Tooltip text="View Server Home">
|
||||
{tooltipProps => (
|
||||
<Button
|
||||
{...tooltipProps}
|
||||
look={Button.Looks.BLANK}
|
||||
size={Button.Sizes.NONE}
|
||||
innerClassName={ChannelRowClasses.icon}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
|
||||
currentShouldViewServerHome = true;
|
||||
for (const setState of shouldViewServerHomeStates) {
|
||||
setState(true);
|
||||
}
|
||||
}}
|
||||
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="m2.4 8.4 8.38-6.46a2 2 0 0 1 2.44 0l8.39 6.45a2 2 0 0 1-.79 3.54l-.32.07-.82 8.2a2 2 0 0 1-1.99 1.8H16a1 1 0 0 1-1-1v-5a3 3 0 0 0-6 0v5a1 1 0 0 1-1 1H6.31a2 2 0 0 1-1.99-1.8L3.5 12l-.32-.07a2 2 0 0 1-.79-3.54Z" />
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function useForceServerHome() {
|
||||
const { forceServerHome } = settings.use(["forceServerHome"]);
|
||||
const [shouldViewServerHome, setShouldViewServerHome] = useState(currentShouldViewServerHome);
|
||||
|
||||
useEffect(() => {
|
||||
shouldViewServerHomeStates.add(setShouldViewServerHome);
|
||||
|
||||
return () => {
|
||||
shouldViewServerHomeStates.delete(setShouldViewServerHome);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return shouldViewServerHome || forceServerHome;
|
||||
}
|
||||
|
||||
function useDisableViewServerHome() {
|
||||
useEffect(() => () => {
|
||||
currentShouldViewServerHome = false;
|
||||
for (const setState of shouldViewServerHomeStates) {
|
||||
setState(false);
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
forceServerHome: {
|
||||
|
@ -30,15 +89,9 @@ const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
function useForceServerHome() {
|
||||
const { forceServerHome } = settings.use(["forceServerHome"]);
|
||||
|
||||
return forceServerHome;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
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],
|
||||
settings,
|
||||
|
||||
|
@ -89,17 +142,40 @@ export default definePlugin({
|
|||
{
|
||||
find: "61eef9_2",
|
||||
replacement: {
|
||||
match: /(?<=getMutableGuildChannelsForGuild\(\i\)\);)(?=if\(null==\i\|\|)/,
|
||||
replace: "if($self.useForceServerHome())return false;"
|
||||
match: /getMutableGuildChannelsForGuild\(\i\);return\(0,\i\.useStateFromStores\).+?\]\)(?=}function)/,
|
||||
replace: m => `${m}&&!$self.useForceServerHome()`
|
||||
}
|
||||
},
|
||||
// Add View Server Home Button to Server Guide
|
||||
{
|
||||
find: "487e85_1",
|
||||
replacement: {
|
||||
match: /(?<=text:(\i)\?\i\.\i\.Messages\.SERVER_GUIDE:\i\.\i\.Messages\.GUILD_HOME,)/,
|
||||
replace: "trailing:$self.ViewServerHomeButton({serverGuide:$1}),"
|
||||
}
|
||||
},
|
||||
// Disable view Server Home override when the Server Home is unmouted
|
||||
{
|
||||
find: "69386d_5",
|
||||
replacement: {
|
||||
match: /location:"69386d_5".+?;/,
|
||||
replace: "$&$self.useDisableViewServerHome();"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
ViewServerHomeButton: ErrorBoundary.wrap(({ serverGuide }: { serverGuide?: boolean; }) => {
|
||||
if (serverGuide !== true) return null;
|
||||
|
||||
return <ViewServerHomeButton />;
|
||||
}),
|
||||
|
||||
useForceServerHome,
|
||||
useDisableViewServerHome,
|
||||
|
||||
contextMenus: {
|
||||
"guild-context"(children, props) {
|
||||
const forceServerHome = useForceServerHome();
|
||||
const { forceServerHome } = settings.use(["forceServerHome"]);
|
||||
|
||||
if (!props?.guild) return;
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import ExpandableHeader from "@components/ExpandableHeader";
|
||||
import { OpenExternalIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Alerts, Menu, Parser, useState } from "@webpack/common";
|
||||
import { Guild, User } from "discord-types/general";
|
||||
|
@ -36,13 +35,26 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
|
|||
import { settings } from "./settings";
|
||||
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(
|
||||
<Menu.MenuItem
|
||||
label="View Reviews"
|
||||
id="vc-rdb-server-reviews"
|
||||
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,
|
||||
contextMenus: {
|
||||
"guild-header-popout": guildPopoutPatch
|
||||
"guild-header-popout": guildPopoutPatch,
|
||||
"guild-context": guildPopoutPatch,
|
||||
"user-context": userContextPatch,
|
||||
"user-profile-actions": userContextPatch
|
||||
},
|
||||
|
||||
patches: [
|
||||
|
@ -75,13 +90,6 @@ export default definePlugin({
|
|||
const s = settings.store;
|
||||
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();
|
||||
|
||||
setTimeout(async () => {
|
||||
|
|
|
@ -71,7 +71,7 @@ export const settings = definePluginSettings({
|
|||
</Button>
|
||||
|
||||
<Button onClick={async () => {
|
||||
let url = "https://reviewdb.mantikafasi.dev/";
|
||||
let url = "https://reviewdb.mantikafasi.dev";
|
||||
const token = await getToken();
|
||||
if (token)
|
||||
url += "/api/redirect?token=" + encodeURIComponent(token);
|
||||
|
|
|
@ -50,7 +50,7 @@ export default definePlugin({
|
|||
patches: [
|
||||
// Chat Mentions
|
||||
{
|
||||
find: "CLYDE_AI_MENTION_COLOR:null,",
|
||||
find: 'location:"UserMention',
|
||||
replacement: [
|
||||
{
|
||||
match: /user:(\i),channel:(\i).{0,400}?"@"\.concat\(.+?\)/,
|
||||
|
@ -94,7 +94,7 @@ export default definePlugin({
|
|||
find: "renderPrioritySpeaker",
|
||||
replacement: [
|
||||
{
|
||||
match: /renderName\(\).{0,100}speaking:.{50,100}jsx.{5,10}{/,
|
||||
match: /renderName\(\){.+?usernameSpeaking\]:.+?(?=children)/,
|
||||
replace: "$&...$self.getVoiceProps(this.props),"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -14,10 +14,11 @@ export default definePlugin({
|
|||
authors: [Devs.AndrewDLO, Devs.FieryFlames],
|
||||
patches: [
|
||||
{
|
||||
find: "call_ringing_beat\"",
|
||||
find: '"call_ringing_beat"',
|
||||
replacement: {
|
||||
match: /500===\i\(\)\.random\(1,1e3\)/,
|
||||
replace: "true"
|
||||
// FIXME Remove === alternative when it hits stable
|
||||
match: /500(!==|===)\i\(\)\.random\(1,1e3\)/,
|
||||
replace: (_, predicate) => predicate === "!==" ? "false" : "true",
|
||||
}
|
||||
},
|
||||
],
|
||||
|
|
|
@ -35,7 +35,7 @@ const Section = findComponentByCodeLazy(".lastSection", "children:");
|
|||
const ThemeStore = findStoreLazy("ThemeStore");
|
||||
const platformHooks: { useLegacyPlatformType(platform: string): string; } = findByPropsLazy("useLegacyPlatformType");
|
||||
const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl");
|
||||
const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"');
|
||||
const getProfileThemeProps = findByCodeLazy(".getPreviewThemeColors", "primaryColor:");
|
||||
|
||||
const enum Spacing {
|
||||
COMPACT,
|
||||
|
@ -74,8 +74,8 @@ interface ConnectionPlatform {
|
|||
icon: { lightSVG: string, darkSVG: string; };
|
||||
}
|
||||
|
||||
const profilePopoutComponent = ErrorBoundary.wrap(({ user, displayProfile }: { user: User, displayProfile; }) =>
|
||||
<ConnectionsComponent id={user.id} theme={getTheme(user, displayProfile).profileTheme} />
|
||||
const profilePopoutComponent = ErrorBoundary.wrap((props: { user: User, displayProfile; }) =>
|
||||
<ConnectionsComponent id={props.user.id} theme={getProfileThemeProps(props).theme} />
|
||||
);
|
||||
|
||||
const profilePanelComponent = ErrorBoundary.wrap(({ id }: { id: string; }) =>
|
||||
|
|
|
@ -36,6 +36,8 @@ const enum ShowMode {
|
|||
HiddenIconWithMutedStyle
|
||||
}
|
||||
|
||||
const CONNECT = 1n << 20n;
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
hideUnreads: {
|
||||
description: "Hide Unreads",
|
||||
|
@ -273,12 +275,12 @@ export default definePlugin({
|
|||
{
|
||||
// Change the role permission check to CONNECT if the channel is locked
|
||||
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
|
||||
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
|
||||
|
@ -452,7 +454,7 @@ export default definePlugin({
|
|||
{
|
||||
// Filter hidden channels from GuildChannelStore.getChannels unless told otherwise
|
||||
match: /(?<=getChannels\(\i)(\){.+?)return (.+?)}/,
|
||||
replace: (_, rest, channels) => `,shouldIncludeHidden${rest}return $self.resolveGuildChannels(${channels},shouldIncludeHidden??false);}`
|
||||
replace: (_, rest, channels) => `,shouldIncludeHidden${rest}return $self.resolveGuildChannels(${channels},shouldIncludeHidden??arguments[0]==="@favorites");}`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
19
src/plugins/showHiddenThings/README.md
Normal file
19
src/plugins/showHiddenThings/README.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# ShowHiddenThings
|
||||
|
||||
Displays various hidden & moderator-only things regardless of permissions.
|
||||
|
||||
## Features
|
||||
|
||||
- Show member timeout icons in chat
|
||||

|
||||
|
||||
- Show the invites paused tooltip in the server list
|
||||

|
||||
|
||||
- Show the member mod view context menu item in all servers
|
||||
|
||||

|
||||
|
||||
- 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
|
109
src/plugins/showHiddenThings/index.ts
Normal file
109
src/plugins/showHiddenThings/index.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
showTimeouts: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show member timeout icons in chat.",
|
||||
default: true,
|
||||
},
|
||||
showInvitesPaused: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show the invites paused tooltip in the server list.",
|
||||
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");
|
||||
export default definePlugin({
|
||||
name: "ShowHiddenThings",
|
||||
tags: ["ShowTimeouts", "ShowInvitesPaused", "ShowModView", "DisableDiscoveryFilters"],
|
||||
description: "Displays various hidden & moderator-only things regardless of permissions.",
|
||||
authors: [Devs.Dolfies],
|
||||
patches: [
|
||||
{
|
||||
find: "showCommunicationDisabledStyles",
|
||||
predicate: () => settings.store.showTimeouts,
|
||||
replacement: {
|
||||
match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/,
|
||||
replace: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
find: "useShouldShowInvitesDisabledNotif:",
|
||||
predicate: () => settings.store.showInvitesPaused,
|
||||
replacement: {
|
||||
match: /\i\.\i\.can\(\i\.Permissions.MANAGE_GUILD,\i\)/,
|
||||
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,
|
||||
});
|
|
@ -7,6 +7,7 @@
|
|||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Message, User } from "discord-types/general";
|
||||
|
@ -49,14 +50,14 @@ export default definePlugin({
|
|||
{
|
||||
find: ".useCanSeeRemixBadge)",
|
||||
replacement: {
|
||||
match: /(?<=onContextMenu:\i,children:).*?\}/,
|
||||
match: /(?<=onContextMenu:\i,children:).*?\)}/,
|
||||
replace: "$self.renderUsername(arguments[0])}"
|
||||
}
|
||||
},
|
||||
],
|
||||
settings,
|
||||
|
||||
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
|
||||
renderUsername: ErrorBoundary.wrap(({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
|
||||
try {
|
||||
const user = userOverride ?? message.author;
|
||||
let { username } = user;
|
||||
|
@ -66,14 +67,14 @@ export default definePlugin({
|
|||
const { nick } = author;
|
||||
const prefix = withMentionPrefix ? "@" : "";
|
||||
if (username === nick || isRepliedMessage && !settings.store.inReplies)
|
||||
return prefix + nick;
|
||||
return <>{prefix}{nick}</>;
|
||||
if (settings.store.mode === "user-nick")
|
||||
return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>;
|
||||
if (settings.store.mode === "nick-user")
|
||||
return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>;
|
||||
return prefix + username;
|
||||
return <>{prefix}{username}</>;
|
||||
} catch {
|
||||
return author?.nick;
|
||||
return <>{author?.nick}</>;
|
||||
}
|
||||
},
|
||||
}, { noop: true }),
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue