mirror of
https://github.com/Vendicated/Vencord
synced 2024-08-26 03:37:47 -04:00
Merge branch 'main' into main
This commit is contained in:
commit
ed200408b3
69 changed files with 2090 additions and 313 deletions
1
.npmrc
1
.npmrc
|
@ -1 +1,2 @@
|
|||
strict-peer-dependencies=false
|
||||
package-manager-strict=false
|
||||
|
|
16
package.json
16
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.8.4",
|
||||
"version": "1.8.5",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
|
@ -19,16 +19,17 @@
|
|||
"scripts": {
|
||||
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
|
||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch",
|
||||
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
||||
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
|
||||
"inject": "node scripts/runInstaller.mjs",
|
||||
"uninject": "node scripts/runInstaller.mjs",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
|
||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||
"testTsc": "tsc --noEmit",
|
||||
"uninject": "node scripts/runInstaller.mjs",
|
||||
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch"
|
||||
"testTsc": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
||||
|
@ -65,11 +66,12 @@
|
|||
"standalone-electron-types": "^1.0.0",
|
||||
"stylelint": "^15.6.0",
|
||||
"stylelint-config-standard": "^33.0.0",
|
||||
"ts-patch": "^3.1.2",
|
||||
"tsx": "^3.12.7",
|
||||
"type-fest": "^3.9.0",
|
||||
"typescript": "^5.0.4",
|
||||
"zip-local": "^0.3.5",
|
||||
"zustand": "^3.7.2"
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-transform-paths": "^3.4.7",
|
||||
"zip-local": "^0.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@9.1.0",
|
||||
"pnpm": {
|
||||
|
|
7
packages/vencord-types/.gitignore
vendored
Normal file
7
packages/vencord-types/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
*
|
||||
!.*ignore
|
||||
!package.json
|
||||
!*.md
|
||||
!prepare.ts
|
||||
!index.d.ts
|
||||
!globals.d.ts
|
4
packages/vencord-types/.npmignore
Normal file
4
packages/vencord-types/.npmignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
prepare.ts
|
||||
.gitignore
|
||||
HOW2PUB.md
|
5
packages/vencord-types/HOW2PUB.md
Normal file
5
packages/vencord-types/HOW2PUB.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# How to publish
|
||||
|
||||
1. run `pnpm generateTypes` in the project root
|
||||
2. bump package.json version
|
||||
3. npm publish
|
11
packages/vencord-types/README.md
Normal file
11
packages/vencord-types/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Vencord Types
|
||||
|
||||
Typings for Vencord's api, published to npm
|
||||
|
||||
```sh
|
||||
npm i @vencord/types
|
||||
|
||||
yarn add @vencord/types
|
||||
|
||||
pnpm add @vencord/types
|
||||
```
|
24
packages/vencord-types/globals.d.ts
vendored
Normal file
24
packages/vencord-types/globals.d.ts
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare global {
|
||||
export var VencordNative: typeof import("./VencordNative").default;
|
||||
export var Vencord: typeof import("./Vencord");
|
||||
}
|
||||
|
||||
export { };
|
5
packages/vencord-types/index.d.ts
vendored
Normal file
5
packages/vencord-types/index.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/* eslint-disable */
|
||||
|
||||
/// <reference path="Vencord.d.ts" />
|
||||
/// <reference path="globals.d.ts" />
|
||||
/// <reference path="modules.d.ts" />
|
28
packages/vencord-types/package.json
Normal file
28
packages/vencord-types/package.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "@vencord/types",
|
||||
"private": false,
|
||||
"version": "0.1.3",
|
||||
"description": "",
|
||||
"types": "index.d.ts",
|
||||
"scripts": {
|
||||
"prepublishOnly": "tsx ./prepare.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Vencord",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"tsx": "^3.12.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"discord-types": "^1.3.26",
|
||||
"standalone-electron-types": "^1.0.0",
|
||||
"type-fest": "^3.5.3"
|
||||
}
|
||||
}
|
47
packages/vencord-types/prepare.ts
Normal file
47
packages/vencord-types/prepare.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { cpSync, moveSync, readdirSync, rmSync } from "fs-extra";
|
||||
import { join } from "path";
|
||||
|
||||
readdirSync(join(__dirname, "src"))
|
||||
.forEach(child => moveSync(join(__dirname, "src", child), join(__dirname, child), { overwrite: true }));
|
||||
|
||||
const VencordSrc = join(__dirname, "..", "..", "src");
|
||||
|
||||
for (const file of ["preload.d.ts", "userplugins", "main", "debug", "src", "browser", "scripts"]) {
|
||||
rmSync(join(__dirname, file), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function copyDtsFiles(from: string, to: string) {
|
||||
for (const file of readdirSync(from, { withFileTypes: true })) {
|
||||
// bad
|
||||
if (from === VencordSrc && file.name === "globals.d.ts") continue;
|
||||
|
||||
const fullFrom = join(from, file.name);
|
||||
const fullTo = join(to, file.name);
|
||||
|
||||
if (file.isDirectory()) {
|
||||
copyDtsFiles(fullFrom, fullTo);
|
||||
} else if (file.name.endsWith(".d.ts")) {
|
||||
cpSync(fullFrom, fullTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
copyDtsFiles(VencordSrc, __dirname);
|
1123
pnpm-lock.yaml
1123
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
packages:
|
||||
- packages/*
|
|
@ -243,19 +243,27 @@ page.on("console", async e => {
|
|||
}
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
console.error(e.text());
|
||||
} else if (level === "error") {
|
||||
const text = await Promise.all(
|
||||
e.args().map(async a => {
|
||||
try {
|
||||
async function getText() {
|
||||
try {
|
||||
return await Promise.all(
|
||||
e.args().map(async a => {
|
||||
return await maybeGetError(a) || await a.jsonValue();
|
||||
} catch (e) {
|
||||
return a.toString();
|
||||
}
|
||||
})
|
||||
).then(a => a.join(" ").trim());
|
||||
})
|
||||
).then(a => a.join(" ").trim());
|
||||
} catch {
|
||||
return e.text();
|
||||
}
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
const text = await getText();
|
||||
|
||||
console.error(text);
|
||||
if (text.includes("A fatal error occurred:")) {
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (level === "error") {
|
||||
const text = await getText();
|
||||
|
||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
||||
console.error("[Unexpected Error]", text);
|
||||
|
@ -303,8 +311,10 @@ async function runtime(token: string) {
|
|||
delete patch.predicate;
|
||||
delete patch.group;
|
||||
|
||||
if (!Array.isArray(patch.replacement))
|
||||
Vencord.Util.canonicalizeFind(patch);
|
||||
if (!Array.isArray(patch.replacement)) {
|
||||
patch.replacement = [patch.replacement];
|
||||
}
|
||||
|
||||
patch.replacement.forEach(r => {
|
||||
delete r.predicate;
|
||||
|
@ -320,22 +330,31 @@ async function runtime(token: string) {
|
|||
|
||||
const validChunks = new Set<string>();
|
||||
const invalidChunks = new Set<string>();
|
||||
const deferredRequires = new Set<string>();
|
||||
|
||||
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
||||
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
||||
|
||||
// True if resolved, false otherwise
|
||||
const chunksSearchPromises = [] as Array<() => boolean>;
|
||||
const lazyChunkRegex = canonicalizeMatch(/Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/g);
|
||||
const chunkIdsRegex = canonicalizeMatch(/\("(.+?)"\)/g);
|
||||
|
||||
const LazyChunkRegex = canonicalizeMatch(/(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\)))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
|
||||
|
||||
async function searchAndLoadLazyChunks(factoryCode: string) {
|
||||
const lazyChunks = factoryCode.matchAll(lazyChunkRegex);
|
||||
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
||||
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
|
||||
|
||||
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
||||
const chunkIds = Array.from(rawChunkIds.matchAll(chunkIdsRegex)).map(m => m[1]);
|
||||
if (chunkIds.length === 0) return;
|
||||
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
|
||||
// the chunk containing the component
|
||||
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
|
||||
|
||||
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIdsArray, rawChunkIdsSingle, entryPoint]) => {
|
||||
const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
|
||||
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Vencord.Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
|
||||
|
||||
if (chunkIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let invalidChunkGroup = false;
|
||||
|
||||
|
@ -371,6 +390,11 @@ async function runtime(token: string) {
|
|||
// Requires the entry points for all valid chunk groups
|
||||
for (const [, entryPoint] of validChunkGroups) {
|
||||
try {
|
||||
if (shouldForceDefer) {
|
||||
deferredRequires.add(entryPoint);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
@ -433,6 +457,11 @@ async function runtime(token: string) {
|
|||
|
||||
await chunksSearchingDone;
|
||||
|
||||
// Require deferred entry points
|
||||
for (const deferredRequire of deferredRequires) {
|
||||
wreq!(deferredRequire as any);
|
||||
}
|
||||
|
||||
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
||||
const allChunks = [] as string[];
|
||||
|
||||
|
@ -512,7 +541,6 @@ async function runtime(token: string) {
|
|||
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
|
||||
} catch (e) {
|
||||
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
export * as Api from "./api";
|
||||
export * as Components from "./components";
|
||||
export * as Plugins from "./plugins";
|
||||
export * as Util from "./utils";
|
||||
export * as QuickCss from "./utils/quickCss";
|
||||
|
|
|
@ -100,6 +100,7 @@ export async function showNotification(data: NotificationData) {
|
|||
const n = new Notification(title, {
|
||||
body,
|
||||
icon,
|
||||
// @ts-expect-error ts is drunk
|
||||
image
|
||||
});
|
||||
n.onclick = onClick;
|
||||
|
|
|
@ -16,10 +16,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./ExpandableHeader.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Text, Tooltip, useState } from "@webpack/common";
|
||||
export const cl = classNameFactory("vc-expandableheader-");
|
||||
import "./ExpandableHeader.css";
|
||||
|
||||
const cl = classNameFactory("vc-expandableheader-");
|
||||
|
||||
export interface ExpandableHeaderProps {
|
||||
onMoreClick?: () => void;
|
||||
|
@ -31,7 +33,7 @@ export interface ExpandableHeaderProps {
|
|||
buttons?: React.ReactNode[];
|
||||
}
|
||||
|
||||
export default function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
|
||||
export function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
|
||||
const [showContent, setShowContent] = useState(defaultState);
|
||||
|
||||
return (
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { CodeBlock } from "@components/CodeBlock";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { Margins } from "@utils/margins";
|
||||
|
@ -47,7 +46,7 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
|
|||
|
||||
interface ReplacementComponentProps {
|
||||
module: [id: number, factory: Function];
|
||||
match: string | RegExp;
|
||||
match: string;
|
||||
replacement: string | ReplaceFn;
|
||||
setReplacementError(error: any): void;
|
||||
}
|
||||
|
@ -58,7 +57,13 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
|||
|
||||
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
||||
const src: string = fact.toString().replaceAll("\n", "");
|
||||
const canonicalMatch = canonicalizeMatch(match);
|
||||
|
||||
try {
|
||||
new RegExp(match);
|
||||
} catch (e) {
|
||||
return ["", [], []];
|
||||
}
|
||||
const canonicalMatch = canonicalizeMatch(new RegExp(match));
|
||||
try {
|
||||
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
||||
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
||||
|
@ -286,6 +291,7 @@ function PatchHelper() {
|
|||
|
||||
const [module, setModule] = React.useState<[number, Function]>();
|
||||
const [findError, setFindError] = React.useState<string>();
|
||||
const [matchError, setMatchError] = React.useState<string>();
|
||||
|
||||
const code = React.useMemo(() => {
|
||||
return `
|
||||
|
@ -300,20 +306,16 @@ function PatchHelper() {
|
|||
}, [parsedFind, match, replacement]);
|
||||
|
||||
function onFindChange(v: string) {
|
||||
setFindError(void 0);
|
||||
setFind(v);
|
||||
}
|
||||
|
||||
function onFindBlur() {
|
||||
try {
|
||||
let parsedFind = find as string | RegExp;
|
||||
if (/^\/.+?\/$/.test(find)) parsedFind = new RegExp(find.slice(1, -1));
|
||||
let parsedFind = v as string | RegExp;
|
||||
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
|
||||
|
||||
setFindError(void 0);
|
||||
setFind(find);
|
||||
setParsedFind(parsedFind);
|
||||
|
||||
if (find.length) {
|
||||
if (v.length) {
|
||||
findCandidates({ find: parsedFind, setModule, setError: setFindError });
|
||||
}
|
||||
} catch (e: any) {
|
||||
|
@ -322,12 +324,13 @@ function PatchHelper() {
|
|||
}
|
||||
|
||||
function onMatchChange(v: string) {
|
||||
setMatch(v);
|
||||
|
||||
try {
|
||||
new RegExp(v);
|
||||
setFindError(void 0);
|
||||
setMatch(v);
|
||||
setMatchError(void 0);
|
||||
} catch (e: any) {
|
||||
setFindError((e as Error).message);
|
||||
setMatchError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -346,21 +349,15 @@ function PatchHelper() {
|
|||
type="text"
|
||||
value={find}
|
||||
onChange={onFindChange}
|
||||
onBlur={onFindBlur}
|
||||
error={findError}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
||||
<CheckedTextInput
|
||||
<TextInput
|
||||
type="text"
|
||||
value={match}
|
||||
onChange={onMatchChange}
|
||||
validate={v => {
|
||||
try {
|
||||
return (new RegExp(v), true);
|
||||
} catch (e) {
|
||||
return (e as Error).message;
|
||||
}
|
||||
}}
|
||||
error={matchError}
|
||||
/>
|
||||
|
||||
<div className={Margins.top8} />
|
||||
|
@ -374,7 +371,7 @@ function PatchHelper() {
|
|||
{module && (
|
||||
<ReplacementComponent
|
||||
module={module}
|
||||
match={new RegExp(match)}
|
||||
match={match}
|
||||
replacement={replacement}
|
||||
setReplacementError={setReplacementError}
|
||||
/>
|
||||
|
|
18
src/components/index.ts
Normal file
18
src/components/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export * from "./Badge";
|
||||
export * from "./CheckedTextInput";
|
||||
export * from "./CodeBlock";
|
||||
export * from "./DonateButton";
|
||||
export { default as ErrorBoundary } from "./ErrorBoundary";
|
||||
export * from "./ErrorCard";
|
||||
export * from "./ExpandableHeader";
|
||||
export * from "./Flex";
|
||||
export * from "./Heart";
|
||||
export * from "./Icons";
|
||||
export * from "./Link";
|
||||
export * from "./Switch";
|
|
@ -73,6 +73,9 @@ if (!IS_VANILLA) {
|
|||
const original = options.webPreferences.preload;
|
||||
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
|
||||
options.webPreferences.sandbox = false;
|
||||
// work around discord unloading when in background
|
||||
options.webPreferences.backgroundThrottling = false;
|
||||
|
||||
if (settings.frameless) {
|
||||
options.frame = false;
|
||||
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
||||
|
@ -128,14 +131,8 @@ if (!IS_VANILLA) {
|
|||
|
||||
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
||||
|
||||
// Monkey patch commandLine to disable WidgetLayering: Fix DevTools context menus https://github.com/electron/electron/issues/38790
|
||||
const originalAppend = app.commandLine.appendSwitch;
|
||||
app.commandLine.appendSwitch = function (...args) {
|
||||
if (args[0] === "disable-features" && !args[1]?.includes("WidgetLayering")) {
|
||||
args[1] += ",WidgetLayering";
|
||||
}
|
||||
return originalAppend.apply(this, args);
|
||||
};
|
||||
// work around discord unloading when in background
|
||||
app.commandLine.appendSwitch("disable-renderer-backgrounding");
|
||||
} else {
|
||||
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
||||
}
|
||||
|
|
2
src/modules.d.ts
vendored
2
src/modules.d.ts
vendored
|
@ -20,7 +20,7 @@
|
|||
/// <reference types="standalone-electron-types"/>
|
||||
|
||||
declare module "~plugins" {
|
||||
const plugins: Record<string, import("@utils/types").Plugin>;
|
||||
const plugins: Record<string, import("./utils/types").Plugin>;
|
||||
export default plugins;
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,6 @@ export default definePlugin({
|
|||
}
|
||||
]
|
||||
},
|
||||
// Discord Canary
|
||||
{
|
||||
find: "Messages.ACTIVITY_SETTINGS",
|
||||
replacement: {
|
||||
|
|
|
@ -73,13 +73,13 @@ export default definePlugin({
|
|||
{
|
||||
find: "instantBatchUpload:function",
|
||||
replacement: {
|
||||
match: /uploadFiles:(.{1,2}),/,
|
||||
match: /uploadFiles:(\i),/,
|
||||
replace:
|
||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
|
||||
},
|
||||
},
|
||||
{
|
||||
find: "message.attachments",
|
||||
find: 'addFilesTo:"message.attachments"',
|
||||
replacement: {
|
||||
match: /(\i.uploadFiles\((\i),)/,
|
||||
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"
|
||||
|
|
5
src/plugins/automodContext/README.md
Normal file
5
src/plugins/automodContext/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# AutomodContext
|
||||
|
||||
Allows you to jump to the messages surrounding an automod hit
|
||||
|
||||
![Visualization](https://github.com/Vendicated/Vencord/assets/61953774/d13740c8-2062-4553-b975-82fd3d6cc08b)
|
73
src/plugins/automodContext/index.tsx
Normal file
73
src/plugins/automodContext/index.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Button, ChannelStore, Text } from "@webpack/common";
|
||||
|
||||
const { selectChannel } = findByPropsLazy("selectChannel", "selectVoiceChannel");
|
||||
|
||||
function jumpToMessage(channelId: string, messageId: string) {
|
||||
const guildId = ChannelStore.getChannel(channelId)?.guild_id;
|
||||
|
||||
selectChannel({
|
||||
guildId,
|
||||
channelId,
|
||||
messageId,
|
||||
jumpType: "INSTANT"
|
||||
});
|
||||
}
|
||||
|
||||
function findChannelId(message: any): string | null {
|
||||
const { embeds: [embed] } = message;
|
||||
const channelField = embed.fields.find(({ rawName }) => rawName === "channel_id");
|
||||
|
||||
if (!channelField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return channelField.rawValue;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "AutomodContext",
|
||||
description: "Allows you to jump to the messages surrounding an automod hit.",
|
||||
authors: [Devs.JohnyTheCarrot],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".Messages.GUILD_AUTOMOD_REPORT_ISSUES",
|
||||
replacement: {
|
||||
match: /\.Messages\.ACTIONS.+?}\)(?=,(\(0.{0,40}\.dot.*?}\)),)/,
|
||||
replace: (m, dot) => `${m},${dot},$self.renderJumpButton({message:arguments[0].message})`
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
renderJumpButton: ErrorBoundary.wrap(({ message }: { message: any; }) => {
|
||||
const channelId = findChannelId(message);
|
||||
|
||||
if (!channelId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
style={{ padding: "2px 8px" }}
|
||||
look={Button.Looks.LINK}
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.LINK}
|
||||
onClick={() => jumpToMessage(channelId, message.id)}
|
||||
>
|
||||
<Text color="text-link" variant="text-xs/normal">
|
||||
Jump to Surrounding
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
}, { noop: true })
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
# BetterRoleContext
|
||||
|
||||
Adds options to copy role color and edit role when right clicking roles in the user profile
|
||||
Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile
|
||||
|
||||
![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326)
|
||||
![](https://github.com/Vendicated/Vencord/assets/45497981/354220a4-09f3-4c5f-a28e-4b19ca775190)
|
||||
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { ImageIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentGuild } from "@utils/discord";
|
||||
import definePlugin from "@utils/types";
|
||||
import { getCurrentGuild, openImageModal } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
|
||||
|
||||
|
@ -34,10 +36,34 @@ function AppearanceIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
roleIconFileFormat: {
|
||||
type: OptionType.SELECT,
|
||||
description: "File format to use when viewing role icons",
|
||||
options: [
|
||||
{
|
||||
label: "png",
|
||||
value: "png",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "webp",
|
||||
value: "webp",
|
||||
},
|
||||
{
|
||||
label: "jpg",
|
||||
value: "jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterRoleContext",
|
||||
description: "Adds options to copy role color / edit role when right clicking roles in the user profile",
|
||||
authors: [Devs.Ven],
|
||||
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
|
||||
authors: [Devs.Ven, Devs.goodbee],
|
||||
|
||||
settings,
|
||||
|
||||
start() {
|
||||
// DeveloperMode needs to be enabled for the context menu to be shown
|
||||
|
@ -63,6 +89,20 @@ export default definePlugin({
|
|||
);
|
||||
}
|
||||
|
||||
if (role.icon) {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="vc-view-role-icon"
|
||||
label="View Role Icon"
|
||||
action={() => {
|
||||
openImageModal(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`);
|
||||
}}
|
||||
icon={ImageIcon}
|
||||
/>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
|
|
5
src/plugins/customidle/README.md
Normal file
5
src/plugins/customidle/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# CustomIdle
|
||||
|
||||
Lets you change the time until your status gets automatically set to idle. You can also prevent idling altogether.
|
||||
|
||||
![Plugin Configuration](https://github.com/Vendicated/Vencord/assets/45801973/4e5259b2-18e0-42e5-b69f-efc672ce1e0b)
|
94
src/plugins/customidle/index.ts
Normal file
94
src/plugins/customidle/index.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Notices } from "@api/index";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { makeRange } from "@components/PluginSettings/components";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
idleTimeout: {
|
||||
description: "Minutes before Discord goes idle (0 to disable auto-idle)",
|
||||
type: OptionType.SLIDER,
|
||||
markers: makeRange(0, 60, 5),
|
||||
default: 10,
|
||||
stickToMarkers: false,
|
||||
restartNeeded: true // Because of the setInterval patch
|
||||
},
|
||||
remainInIdle: {
|
||||
description: "When you come back to Discord, remain idle until you confirm you want to go online",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "CustomIdle",
|
||||
description: "Allows you to set the time before Discord goes idle (or disable auto-idle)",
|
||||
authors: [Devs.newwares],
|
||||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: "IDLE_DURATION:function(){return",
|
||||
replacement: {
|
||||
match: /(IDLE_DURATION:function\(\){return )\i/,
|
||||
replace: "$1$self.getIdleTimeout()"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: 'type:"IDLE",idle:',
|
||||
replacement: [
|
||||
{
|
||||
match: /Math\.min\((\i\.AfkTimeout\.getSetting\(\)\*\i\.default\.Millis\.SECOND),\i\.IDLE_DURATION\)/,
|
||||
replace: "$1" // Decouple idle from afk (phone notifications will remain at user setting or 10 min maximum)
|
||||
},
|
||||
{
|
||||
match: /\i\.default\.dispatch\({type:"IDLE",idle:!1}\)/,
|
||||
replace: "$self.handleOnline()"
|
||||
},
|
||||
{
|
||||
match: /(setInterval\(\i,\.25\*)\i\.IDLE_DURATION/,
|
||||
replace: "$1$self.getIntervalDelay()" // For web installs
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
getIntervalDelay() {
|
||||
return Math.min(6e5, this.getIdleTimeout());
|
||||
},
|
||||
|
||||
handleOnline() {
|
||||
if (!settings.store.remainInIdle) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "IDLE",
|
||||
idle: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const backOnlineMessage = "Welcome back! Click the button to go online. Click the X to stay idle until reload.";
|
||||
if (
|
||||
Notices.currentNotice?.[1] === backOnlineMessage ||
|
||||
Notices.noticesQueue.some(([, noticeMessage]) => noticeMessage === backOnlineMessage)
|
||||
) return;
|
||||
|
||||
Notices.showNotice(backOnlineMessage, "Exit idle", () => {
|
||||
Notices.popNotice();
|
||||
FluxDispatcher.dispatch({
|
||||
type: "IDLE",
|
||||
idle: false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getIdleTimeout() { // milliseconds, default is 6e5
|
||||
const { idleTimeout } = settings.store;
|
||||
return idleTimeout === 0 ? Infinity : idleTimeout * 60000;
|
||||
}
|
||||
});
|
|
@ -9,7 +9,6 @@ import { proxyLazy } from "@utils/lazy";
|
|||
import { Logger } from "@utils/Logger";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { OAuth2AuthorizeModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from "@webpack/common";
|
||||
import type { StateStorage } from "zustand/middleware";
|
||||
|
||||
import { AUTHORIZE_URL, CLIENT_ID } from "../constants";
|
||||
|
||||
|
@ -23,7 +22,7 @@ interface AuthorizationState {
|
|||
isAuthorized: () => boolean;
|
||||
}
|
||||
|
||||
const indexedDBStorage: StateStorage = {
|
||||
const indexedDBStorage = {
|
||||
async getItem(name: string): Promise<string | null> {
|
||||
return DataStore.get(name).then(v => v ?? null);
|
||||
},
|
||||
|
@ -36,9 +35,9 @@ const indexedDBStorage: StateStorage = {
|
|||
};
|
||||
|
||||
// TODO: Move switching accounts subscription inside the store?
|
||||
export const useAuthorizationStore = proxyLazy(() => zustandCreate<AuthorizationState>(
|
||||
export const useAuthorizationStore = proxyLazy(() => zustandCreate(
|
||||
zustandPersist(
|
||||
(set, get) => ({
|
||||
(set: any, get: any) => ({
|
||||
token: null,
|
||||
tokens: {},
|
||||
init: () => { set({ token: get().tokens[UserStore.getCurrentUser().id] ?? null }); },
|
||||
|
@ -91,7 +90,7 @@ export const useAuthorizationStore = proxyLazy(() => zustandCreate<Authorization
|
|||
));
|
||||
},
|
||||
isAuthorized: () => !!get().token,
|
||||
}),
|
||||
} as AuthorizationState),
|
||||
{
|
||||
name: "decor-auth",
|
||||
getStorage: () => indexedDBStorage,
|
||||
|
|
|
@ -21,7 +21,7 @@ interface UserDecorationsState {
|
|||
clear: () => void;
|
||||
}
|
||||
|
||||
export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate<UserDecorationsState>((set, get) => ({
|
||||
export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({
|
||||
decorations: [],
|
||||
selectedDecoration: null,
|
||||
async fetch() {
|
||||
|
@ -53,4 +53,4 @@ export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate<User
|
|||
useUsersDecorationsStore.getState().set(UserStore.getCurrentUser().id, decoration ? decorationToAsset(decoration) : null);
|
||||
},
|
||||
clear: () => set({ decorations: [], selectedDecoration: null })
|
||||
})));
|
||||
} as UserDecorationsState)));
|
||||
|
|
|
@ -30,7 +30,7 @@ interface UsersDecorationsState {
|
|||
set: (userId: string, decoration: string | null) => void;
|
||||
}
|
||||
|
||||
export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecorationsState>((set, get) => ({
|
||||
export const useUsersDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({
|
||||
usersDecorations: new Map<string, UserDecorationData>(),
|
||||
fetchQueue: new Set(),
|
||||
bulkFetch: debounce(async () => {
|
||||
|
@ -40,7 +40,7 @@ export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecor
|
|||
|
||||
set({ fetchQueue: new Set() });
|
||||
|
||||
const fetchIds = Array.from(fetchQueue);
|
||||
const fetchIds = [...fetchQueue];
|
||||
const fetchedUsersDecorations = await getUsersDecorations(fetchIds);
|
||||
|
||||
const newUsersDecorations = new Map(usersDecorations);
|
||||
|
@ -92,7 +92,7 @@ export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecor
|
|||
newUsersDecorations.set(userId, { asset: decoration, fetchedAt: new Date() });
|
||||
set({ usersDecorations: newUsersDecorations });
|
||||
}
|
||||
})));
|
||||
} as UsersDecorationsState)));
|
||||
|
||||
export function useUserDecorAvatarDecoration(user?: User): AvatarDecoration | null | undefined {
|
||||
const [decorAvatarDecoration, setDecorAvatarDecoration] = useState<string | null>(user ? useUsersDecorationsStore.getState().getAsset(user.id) ?? null : null);
|
||||
|
|
|
@ -15,7 +15,7 @@ import { openChangeDecorationModal } from "../modals/ChangeDecorationModal";
|
|||
|
||||
const CustomizationSection = findByCodeLazy(".customizationSectionBackground");
|
||||
|
||||
interface DecorSectionProps {
|
||||
export interface DecorSectionProps {
|
||||
hideTitle?: boolean;
|
||||
hideDivider?: boolean;
|
||||
noMargin?: boolean;
|
||||
|
|
|
@ -25,7 +25,7 @@ import { Logger } from "@utils/Logger";
|
|||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
|
||||
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
|
||||
import type { CustomEmoji } from "@webpack/types";
|
||||
import type { Emoji } from "@webpack/types";
|
||||
import type { Message } from "discord-types/general";
|
||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
|
@ -54,16 +54,22 @@ const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoCla
|
|||
|
||||
|
||||
const enum EmojiIntentions {
|
||||
REACTION = 0,
|
||||
STATUS = 1,
|
||||
COMMUNITY_CONTENT = 2,
|
||||
CHAT = 3,
|
||||
GUILD_STICKER_RELATED_EMOJI = 4,
|
||||
GUILD_ROLE_BENEFIT_EMOJI = 5,
|
||||
COMMUNITY_CONTENT_ONLY = 6,
|
||||
SOUNDBOARD = 7
|
||||
REACTION,
|
||||
STATUS,
|
||||
COMMUNITY_CONTENT,
|
||||
CHAT,
|
||||
GUILD_STICKER_RELATED_EMOJI,
|
||||
GUILD_ROLE_BENEFIT_EMOJI,
|
||||
COMMUNITY_CONTENT_ONLY,
|
||||
SOUNDBOARD,
|
||||
VOICE_CHANNEL_TOPIC,
|
||||
GIFT,
|
||||
AUTO_SUGGESTION,
|
||||
POLLS
|
||||
}
|
||||
|
||||
const IS_BYPASSEABLE_INTENTION = `[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`;
|
||||
|
||||
const enum StickerType {
|
||||
PNG = 1,
|
||||
APNG = 2,
|
||||
|
@ -198,37 +204,43 @@ export default definePlugin({
|
|||
patches: [
|
||||
{
|
||||
find: ".PREMIUM_LOCKED;",
|
||||
group: true,
|
||||
predicate: () => settings.store.enableEmojiBypass,
|
||||
replacement: [
|
||||
{
|
||||
// Create a variable for the intention of listing the emoji
|
||||
match: /(?<=,intention:(\i).+?;)/,
|
||||
replace: (_, intention) => `let fakeNitroIntention=${intention};`
|
||||
// Create a variable for the intention of using the emoji
|
||||
match: /(?<=\.USE_EXTERNAL_EMOJIS.+?;)(?<=intention:(\i).+?)/,
|
||||
replace: (_, intention) => `const fakeNitroIntention=${intention};`
|
||||
},
|
||||
{
|
||||
// Send the intention of listing the emoji to the nitro permission check functions
|
||||
match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g,
|
||||
replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0'
|
||||
// Disallow the emoji for external if the intention doesn't allow it
|
||||
match: /&&!\i&&!\i(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
||||
replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`
|
||||
},
|
||||
{
|
||||
// Disallow the emoji if the intention doesn't allow it
|
||||
match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
||||
replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))`
|
||||
// Disallow the emoji for unavailable if the intention doesn't allow it
|
||||
match: /!\i\.available(?=\)return \i\.\i\.GUILD_SUBSCRIPTION_UNAVAILABLE;)/,
|
||||
replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`
|
||||
},
|
||||
{
|
||||
// Make the emoji always available if the intention allows it
|
||||
match: /if\(!\i\.available/,
|
||||
replace: m => `${m}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
|
||||
// Disallow the emoji for premium locked if the intention doesn't allow it
|
||||
match: /!\i\.\i\.canUseEmojisEverywhere\(\i\)/,
|
||||
replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
|
||||
},
|
||||
{
|
||||
// Allow animated emojis to be used if the intention allows it
|
||||
match: /(?<=\|\|)\i\.\i\.canUseAnimatedEmojis\(\i\)/,
|
||||
replace: m => `(${m}||${IS_BYPASSEABLE_INTENTION})`
|
||||
}
|
||||
]
|
||||
},
|
||||
// Allow emojis and animated emojis to be sent everywhere
|
||||
// Allows the usage of subscription-locked emojis
|
||||
{
|
||||
find: "canUseAnimatedEmojis:function",
|
||||
predicate: () => settings.store.enableEmojiBypass,
|
||||
find: "isUnusableRoleSubscriptionEmoji:function",
|
||||
replacement: {
|
||||
match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))(?=})/g,
|
||||
replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
||||
match: /isUnusableRoleSubscriptionEmoji:function/,
|
||||
// Replace the original export with a func that always returns false and alias the original
|
||||
replace: "isUnusableRoleSubscriptionEmoji:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function"
|
||||
}
|
||||
},
|
||||
// Allow stickers to be sent everywhere
|
||||
|
@ -242,10 +254,10 @@ export default definePlugin({
|
|||
},
|
||||
// Make stickers always available
|
||||
{
|
||||
find: "\"SENDABLE\"",
|
||||
find: '"SENDABLE"',
|
||||
predicate: () => settings.store.enableStickerBypass,
|
||||
replacement: {
|
||||
match: /(\w+)\.available\?/,
|
||||
match: /\i\.available\?/,
|
||||
replace: "true?"
|
||||
}
|
||||
},
|
||||
|
@ -408,15 +420,6 @@ export default definePlugin({
|
|||
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"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -809,8 +812,8 @@ export default definePlugin({
|
|||
UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DraftType.ChannelMessage);
|
||||
},
|
||||
|
||||
canUseEmote(e: CustomEmoji, channelId: string) {
|
||||
if (e.require_colons === false) return true;
|
||||
canUseEmote(e: Emoji, channelId: string) {
|
||||
if (e.type === "UNICODE") return true;
|
||||
if (e.available === false) return false;
|
||||
|
||||
const isUnusableRoleSubEmoji = RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
|
||||
|
|
3
src/plugins/fakeProfileThemes/index.css
Normal file
3
src/plugins/fakeProfileThemes/index.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.vc-fpt-preview * {
|
||||
pointer-events: none;
|
||||
}
|
|
@ -17,13 +17,17 @@
|
|||
*/
|
||||
|
||||
// This plugin is a port from Alyxia's Vendetta plugin
|
||||
import "./index.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { copyWithToast } from "@utils/misc";
|
||||
import { classes, copyWithToast } from "@utils/misc";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Button, Forms } from "@webpack/common";
|
||||
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
import virtualMerge from "virtual-merge";
|
||||
|
||||
|
@ -81,6 +85,34 @@ const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
interface ColorPickerProps {
|
||||
color: number | null;
|
||||
label: React.ReactElement;
|
||||
showEyeDropper?: boolean;
|
||||
suggestedColors?: string[];
|
||||
onChange(value: number | null): void;
|
||||
}
|
||||
|
||||
// I can't be bothered to figure out the semantics of this component. The
|
||||
// functions surely get some event argument sent to them and they likely aren't
|
||||
// all required. If anyone who wants to use this component stumbles across this
|
||||
// code, you'll have to do the research yourself.
|
||||
interface ProfileModalProps {
|
||||
user: User;
|
||||
pendingThemeColors: [number, number];
|
||||
onAvatarChange: () => void;
|
||||
onBannerChange: () => void;
|
||||
canUsePremiumCustomization: boolean;
|
||||
hideExampleButton: boolean;
|
||||
hideFakeActivity: boolean;
|
||||
isTryItOutFlow: boolean;
|
||||
}
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
|
||||
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>('"ProfileCustomizationPreview"');
|
||||
|
||||
const requireColorPicker = extractAndLoadChunksLazy(["USER_SETTINGS_PROFILE_COLOR_DEFAULT_BUTTON.format"], /createPromise:\(\)=>\i\.\i\("(.+?)"\).then\(\i\.bind\(\i,"(.+?)"\)\)/);
|
||||
|
||||
export default definePlugin({
|
||||
name: "FakeProfileThemes",
|
||||
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
|
||||
|
@ -101,21 +133,98 @@ export default definePlugin({
|
|||
}
|
||||
}
|
||||
],
|
||||
settingsAboutComponent: () => (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
After enabling this plugin, you will see custom colors in the profiles of other people using compatible plugins. <br />
|
||||
To set your own colors:
|
||||
<ul>
|
||||
<li>• go to your profile settings</li>
|
||||
<li>• choose your own colors in the Nitro preview</li>
|
||||
<li>• click the "Copy 3y3" button</li>
|
||||
<li>• paste the invisible text anywhere in your bio</li>
|
||||
</ul><br />
|
||||
<b>Please note:</b> if you are using a theme which hides nitro ads, you should disable it temporarily to set colors.
|
||||
</Forms.FormText>
|
||||
</Forms.FormSection>),
|
||||
settingsAboutComponent: () => {
|
||||
const existingColors = decode(
|
||||
UserProfileStore.getUserProfile(UserStore.getCurrentUser().id).bio
|
||||
) ?? [0, 0];
|
||||
const [color1, setColor1] = useState(existingColors[0]);
|
||||
const [color2, setColor2] = useState(existingColors[1]);
|
||||
|
||||
const [, , loadingColorPickerChunk] = useAwaiter(requireColorPicker);
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
After enabling this plugin, you will see custom colors in
|
||||
the profiles of other people using compatible plugins.{" "}
|
||||
<br />
|
||||
To set your own colors:
|
||||
<ul>
|
||||
<li>
|
||||
• use the color pickers below to choose your colors
|
||||
</li>
|
||||
<li>• click the "Copy 3y3" button</li>
|
||||
<li>• paste the invisible text anywhere in your bio</li>
|
||||
</ul><br />
|
||||
<Forms.FormDivider
|
||||
className={classes(Margins.top8, Margins.bottom8)}
|
||||
/>
|
||||
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
|
||||
{!loadingColorPickerChunk && (
|
||||
<Flex
|
||||
direction={Flex.Direction.HORIZONTAL}
|
||||
style={{ gap: "1rem" }}
|
||||
>
|
||||
<ColorPicker
|
||||
color={color1}
|
||||
label={
|
||||
<Text
|
||||
variant={"text-xs/normal"}
|
||||
style={{ marginTop: "4px" }}
|
||||
>
|
||||
Primary
|
||||
</Text>
|
||||
}
|
||||
onChange={(color: number) => {
|
||||
setColor1(color);
|
||||
}}
|
||||
/>
|
||||
<ColorPicker
|
||||
color={color2}
|
||||
label={
|
||||
<Text
|
||||
variant={"text-xs/normal"}
|
||||
style={{ marginTop: "4px" }}
|
||||
>
|
||||
Accent
|
||||
</Text>
|
||||
}
|
||||
onChange={(color: number) => {
|
||||
setColor2(color);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const colorString = encode(color1, color2);
|
||||
copyWithToast(colorString);
|
||||
}}
|
||||
color={Button.Colors.PRIMARY}
|
||||
size={Button.Sizes.XLARGE}
|
||||
>
|
||||
Copy 3y3
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
<Forms.FormDivider
|
||||
className={classes(Margins.top8, Margins.bottom8)}
|
||||
/>
|
||||
<Forms.FormTitle tag="h3">Preview</Forms.FormTitle>
|
||||
<div className="vc-fpt-preview">
|
||||
<ProfileModal
|
||||
user={UserStore.getCurrentUser()}
|
||||
pendingThemeColors={[color1, color2]}
|
||||
onAvatarChange={() => { }}
|
||||
onBannerChange={() => { }}
|
||||
canUsePremiumCustomization={true}
|
||||
hideExampleButton={true}
|
||||
hideFakeActivity={true}
|
||||
isTryItOutFlow={true}
|
||||
/>
|
||||
</div>
|
||||
</Forms.FormText>
|
||||
</Forms.FormSection>);
|
||||
},
|
||||
settings,
|
||||
colorDecodeHook(user: UserProfile) {
|
||||
if (user) {
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentChannel } from "@utils/discord";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Heading, React, RelationshipStore, Text } from "@webpack/common";
|
||||
|
@ -22,6 +24,7 @@ export default definePlugin({
|
|||
description: "Shows when you became friends with someone in the user popout",
|
||||
authors: [Devs.Elvyra],
|
||||
patches: [
|
||||
// User popup
|
||||
{
|
||||
find: ".AnalyticsSections.USER_PROFILE}",
|
||||
replacement: {
|
||||
|
@ -29,16 +32,34 @@ export default definePlugin({
|
|||
replace: "$&,$self.friendsSince({ userId: $1 })"
|
||||
}
|
||||
},
|
||||
// User DMs "User Profile" popup in the right
|
||||
{
|
||||
find: ".UserPopoutUpsellSource.PROFILE_PANEL,",
|
||||
replacement: {
|
||||
match: /\i.default,\{userId:(\i)}\)/,
|
||||
replace: "$&,$self.friendsSince({ userId: $1 })"
|
||||
}
|
||||
},
|
||||
// User Profile Modal
|
||||
{
|
||||
find: ".userInfoSectionHeader,",
|
||||
replacement: {
|
||||
match: /(\.Messages\.USER_PROFILE_MEMBER_SINCE.+?userId:(.+?),textClassName:)(\i\.userInfoText)}\)/,
|
||||
replace: (_, rest, userId, textClassName) => `${rest}!$self.getFriendSince(${userId}) ? ${textClassName} : void 0 }), $self.friendsSince({ userId: ${userId}, textClassName: ${textClassName} })`
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
friendsSince: ErrorBoundary.wrap(({ userId }: { userId: string; }) => {
|
||||
getFriendSince(userId: string) {
|
||||
try {
|
||||
return RelationshipStore.getSince(userId);
|
||||
} catch (err) {
|
||||
new Logger("FriendsSince").error(err);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
friendsSince: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => {
|
||||
const friendsSince = RelationshipStore.getSince(userId);
|
||||
if (!friendsSince) return null;
|
||||
|
||||
|
@ -61,7 +82,7 @@ export default definePlugin({
|
|||
<path d="M3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z" />
|
||||
</svg>
|
||||
)}
|
||||
<Text variant="text-sm/normal" className={clydeMoreInfo.body}>
|
||||
<Text variant="text-sm/normal" className={classes(clydeMoreInfo.body, textClassName)}>
|
||||
{getCreatedAtDate(friendsSince, locale.getLocale())}
|
||||
</Text>
|
||||
</div>
|
||||
|
@ -69,4 +90,3 @@ export default definePlugin({
|
|||
);
|
||||
}, { noop: true })
|
||||
});
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import { registerCommand, unregisterCommand } from "@api/Commands";
|
|||
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { canonicalizeFind } from "@utils/patches";
|
||||
import { Patch, Plugin, StartAt } from "@utils/types";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
import { FluxEvents } from "@webpack/types";
|
||||
|
@ -83,8 +84,12 @@ for (const p of pluginsValues) {
|
|||
if (p.patches && isPluginEnabled(p.name)) {
|
||||
for (const patch of p.patches) {
|
||||
patch.plugin = p.name;
|
||||
if (!Array.isArray(patch.replacement))
|
||||
|
||||
canonicalizeFind(patch);
|
||||
if (!Array.isArray(patch.replacement)) {
|
||||
patch.replacement = [patch.replacement];
|
||||
}
|
||||
|
||||
patches.push(patch);
|
||||
}
|
||||
}
|
||||
|
@ -165,13 +170,14 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
}
|
||||
try {
|
||||
p.start();
|
||||
p.started = true;
|
||||
} catch (e) {
|
||||
logger.error(`Failed to start ${name}\n`, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
p.started = true;
|
||||
|
||||
if (commands?.length) {
|
||||
logger.debug("Registering commands of plugin", name);
|
||||
for (const cmd of commands) {
|
||||
|
@ -201,6 +207,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
|
||||
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
|
||||
const { name, commands, flux, contextMenus } = p;
|
||||
|
||||
if (p.stop) {
|
||||
logger.info("Stopping plugin", name);
|
||||
if (!p.started) {
|
||||
|
@ -209,13 +216,14 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
|||
}
|
||||
try {
|
||||
p.stop();
|
||||
p.started = false;
|
||||
} catch (e) {
|
||||
logger.error(`Failed to stop ${name}\n`, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
p.started = false;
|
||||
|
||||
if (commands?.length) {
|
||||
logger.debug("Unregistering commands of plugin", name);
|
||||
for (const cmd of commands) {
|
||||
|
|
|
@ -114,6 +114,11 @@ const settings = definePluginSettings({
|
|||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
},
|
||||
shareSong: {
|
||||
description: "show link to song on last.fm",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
hideWithSpotify: {
|
||||
description: "hide last.fm presence if spotify is running",
|
||||
type: OptionType.BOOLEAN,
|
||||
|
@ -295,12 +300,7 @@ export default definePlugin({
|
|||
large_text: trackData.album || undefined,
|
||||
};
|
||||
|
||||
const buttons: ActivityButton[] = [
|
||||
{
|
||||
label: "View Song",
|
||||
url: trackData.url,
|
||||
},
|
||||
];
|
||||
const buttons: ActivityButton[] = [];
|
||||
|
||||
if (settings.store.shareUsername)
|
||||
buttons.push({
|
||||
|
@ -308,6 +308,12 @@ export default definePlugin({
|
|||
url: `https://www.last.fm/user/${settings.store.username}`,
|
||||
});
|
||||
|
||||
if (settings.store.shareSong)
|
||||
buttons.push({
|
||||
label: "View Song",
|
||||
url: trackData.url,
|
||||
});
|
||||
|
||||
const statusName = (() => {
|
||||
switch (settings.store.nameFormat) {
|
||||
case NameFormat.ArtistFirst:
|
||||
|
@ -333,7 +339,7 @@ export default definePlugin({
|
|||
state: trackData.artist,
|
||||
assets,
|
||||
|
||||
buttons: buttons.map(v => v.label),
|
||||
buttons: buttons.length ? buttons.map(v => v.label) : undefined,
|
||||
metadata: {
|
||||
button_urls: buttons.map(v => v.url),
|
||||
},
|
||||
|
|
|
@ -22,9 +22,10 @@ interface Diff {
|
|||
hours: number,
|
||||
minutes: number,
|
||||
seconds: number;
|
||||
milliseconds: number;
|
||||
}
|
||||
|
||||
const DISCORD_KT_DELAY = 1471228.928;
|
||||
const DISCORD_KT_DELAY = 1471228928;
|
||||
const HiddenVisually = findExportedComponentLazy("HiddenVisually");
|
||||
|
||||
export default definePlugin({
|
||||
|
@ -42,6 +43,11 @@ export default definePlugin({
|
|||
type: OptionType.BOOLEAN,
|
||||
description: "Detect old Discord Android clients",
|
||||
default: true
|
||||
},
|
||||
showMillis: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show milliseconds",
|
||||
default: false
|
||||
}
|
||||
}),
|
||||
|
||||
|
@ -55,12 +61,13 @@ export default definePlugin({
|
|||
}
|
||||
],
|
||||
|
||||
stringDelta(delta: number) {
|
||||
stringDelta(delta: number, showMillis: boolean) {
|
||||
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),
|
||||
days: Math.round(delta / (60 * 60 * 24 * 1000)),
|
||||
hours: Math.round((delta / (60 * 60 * 1000)) % 24),
|
||||
minutes: Math.round((delta / (60 * 1000)) % 60),
|
||||
seconds: Math.round(delta / 1000 % 60),
|
||||
milliseconds: Math.round(delta % 1000)
|
||||
};
|
||||
|
||||
const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;
|
||||
|
@ -72,7 +79,7 @@ export default definePlugin({
|
|||
return prev + (
|
||||
isNonNullish(s)
|
||||
? (prev !== ""
|
||||
? k === "seconds"
|
||||
? (showMillis ? k === "milliseconds" : k === "seconds")
|
||||
? " and "
|
||||
: " "
|
||||
: "") + s
|
||||
|
@ -84,18 +91,21 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
latencyTooltipData(message: Message) {
|
||||
const { latency, detectDiscordKotlin } = this.settings.store;
|
||||
const { latency, detectDiscordKotlin, showMillis } = this.settings.store;
|
||||
const { id, nonce } = message;
|
||||
|
||||
// Message wasn't received through gateway
|
||||
if (!isNonNullish(nonce)) return null;
|
||||
|
||||
let isDiscordKotlin = false;
|
||||
let delta = Math.round((SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce)) / 1000);
|
||||
let delta = SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce); // milliseconds
|
||||
if (!showMillis) {
|
||||
delta = Math.round(delta / 1000) * 1000;
|
||||
}
|
||||
|
||||
// Old Discord Android clients have a delay of around 17 days
|
||||
// This is a workaround for that
|
||||
if (-delta >= DISCORD_KT_DELAY - 86400) { // One day of padding for good measure
|
||||
if (-delta >= DISCORD_KT_DELAY - 86400000) { // One day of padding for good measure
|
||||
isDiscordKotlin = detectDiscordKotlin;
|
||||
delta += DISCORD_KT_DELAY;
|
||||
}
|
||||
|
@ -105,22 +115,23 @@ export default definePlugin({
|
|||
// Can't do anything if the clock is behind
|
||||
const abs = Math.abs(delta);
|
||||
const ahead = abs !== delta;
|
||||
const latencyMillis = latency * 1000;
|
||||
|
||||
const stringDelta = abs >= latency ? this.stringDelta(abs) : null;
|
||||
const stringDelta = abs >= latencyMillis ? this.stringDelta(abs, showMillis) : null;
|
||||
|
||||
// Also thanks dziurwa
|
||||
// 2 minutes
|
||||
const TROLL_LIMIT = 2 * 60;
|
||||
const TROLL_LIMIT = 2 * 60 * 1000;
|
||||
|
||||
const fill: Fill = isDiscordKotlin
|
||||
? ["status-positive", "status-positive", "text-muted"]
|
||||
: delta >= TROLL_LIMIT || ahead
|
||||
? ["text-muted", "text-muted", "text-muted"]
|
||||
: delta >= (latency * 2)
|
||||
: delta >= (latencyMillis * 2)
|
||||
? ["status-danger", "text-muted", "text-muted"]
|
||||
: ["status-warning", "status-warning", "text-muted"];
|
||||
|
||||
return (abs >= latency || isDiscordKotlin) ? { delta: stringDelta, ahead, fill, isDiscordKotlin } : null;
|
||||
return (abs >= latencyMillis || isDiscordKotlin) ? { delta: stringDelta, ahead, fill, isDiscordKotlin } : null;
|
||||
},
|
||||
|
||||
Tooltip() {
|
||||
|
|
|
@ -227,10 +227,8 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
|
|||
|
||||
const accessories = [] as (JSX.Element | null)[];
|
||||
|
||||
let match = null as RegExpMatchArray | null;
|
||||
while ((match = messageLinkRegex.exec(message.content!)) !== null) {
|
||||
const [_, channelID, messageID] = match;
|
||||
if (embeddedBy.includes(messageID)) {
|
||||
for (const [_, channelID, messageID] of message.content!.matchAll(messageLinkRegex)) {
|
||||
if (embeddedBy.includes(messageID) || embeddedBy.length > 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -295,12 +295,9 @@ export default definePlugin({
|
|||
// },
|
||||
{
|
||||
// Pass through editHistory & deleted & original attachments to the "edited message" transformer
|
||||
match: /interactionData:(\i)\.interactionData/,
|
||||
match: /(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/,
|
||||
replace:
|
||||
"interactionData:$1.interactionData," +
|
||||
"deleted:$1.deleted," +
|
||||
"editHistory:$1.editHistory," +
|
||||
"attachments:$1.attachments"
|
||||
"Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, attachments:$1.attachments })"
|
||||
},
|
||||
|
||||
// {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
.emoji,
|
||||
[data-type="sticker"],
|
||||
iframe,
|
||||
.messagelogger-deleted-attachment,
|
||||
.messagelogger-deleted-attachment:not([class*="hiddenAttachment_"]),
|
||||
[class|="inlineMediaEmbed"]
|
||||
) {
|
||||
filter: grayscale(1) !important;
|
||||
|
|
|
@ -354,6 +354,15 @@ export default definePlugin({
|
|||
if (location === "chat" && !settings.tagSettings[tag.name].showInChat) continue;
|
||||
if (location === "not-chat" && !settings.tagSettings[tag.name].showInNotChat) continue;
|
||||
|
||||
// If the owner tag is disabled, and the user is the owner of the guild,
|
||||
// avoid adding other tags because the owner will always match the condition for them
|
||||
if (
|
||||
tag.name !== "OWNER" &&
|
||||
GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id &&
|
||||
(location === "chat" && !settings.tagSettings.OWNER.showInChat) ||
|
||||
(location === "not-chat" && !settings.tagSettings.OWNER.showInNotChat)
|
||||
) continue;
|
||||
|
||||
if (
|
||||
tag.permissions?.some(perm => perms.includes(perm)) ||
|
||||
(tag.condition?.(message!, user, channel))
|
||||
|
|
5
src/plugins/noDefaultHangStatus/README.md
Normal file
5
src/plugins/noDefaultHangStatus/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# NoDefaultHangStatus
|
||||
|
||||
Disable the default hang status when joining voice channels
|
||||
|
||||
![Visualization](https://github.com/Vendicated/Vencord/assets/24937357/329a9742-236f-48f7-94ff-c3510eca505a)
|
24
src/plugins/noDefaultHangStatus/index.ts
Normal file
24
src/plugins/noDefaultHangStatus/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "NoDefaultHangStatus",
|
||||
description: "Disable the default hang status when joining voice channels",
|
||||
authors: [Devs.D3SOX],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "HangStatusTypes.CHILLING)",
|
||||
replacement: {
|
||||
match: /{enableHangStatus:(\i),/,
|
||||
replace: "{_enableHangStatus:$1=false,"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
|
@ -41,8 +41,9 @@ const settings = definePluginSettings({
|
|||
},
|
||||
});
|
||||
|
||||
migratePluginSettings("PartyMode", "Party mode 🎉");
|
||||
export default definePlugin({
|
||||
name: "Party mode 🎉",
|
||||
name: "PartyMode",
|
||||
description: "Allows you to use party mode cause the party never ends ✨",
|
||||
authors: [Devs.UwUDev],
|
||||
settings,
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import ExpandableHeader from "@components/ExpandableHeader";
|
||||
import { ExpandableHeader } from "@components/ExpandableHeader";
|
||||
import { classes } from "@utils/misc";
|
||||
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
|
||||
import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/common";
|
||||
|
|
|
@ -24,6 +24,7 @@ import { ChannelStore, FluxDispatcher as Dispatcher, MessageStore, PermissionsBi
|
|||
import { Message } from "discord-types/general";
|
||||
|
||||
const Kangaroo = findByPropsLazy("jumpToMessage");
|
||||
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
|
||||
|
||||
const isMac = navigator.platform.includes("Mac"); // bruh
|
||||
let replyIdx = -1;
|
||||
|
@ -139,6 +140,10 @@ function getNextMessage(isUp: boolean, isReply: boolean) {
|
|||
messages = messages.filter(m => m.author.id === meId);
|
||||
}
|
||||
|
||||
if (Vencord.Plugins.isPluginEnabled("NoBlockedMessages")) {
|
||||
messages = messages.filter(m => !RelationshipStore.isBlocked(m.author.id));
|
||||
}
|
||||
|
||||
const mutate = (i: number) => isUp
|
||||
? Math.min(messages.length - 1, i + 1)
|
||||
: Math.max(-1, i - 1);
|
||||
|
|
|
@ -22,14 +22,34 @@ import { addServerListElement, removeServerListElement, ServerListRenderPosition
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
|
||||
interface ThreadJoined {
|
||||
channel: Channel;
|
||||
joinTimestamp: number;
|
||||
}
|
||||
|
||||
type ThreadsJoined = Record<string, ThreadJoined>;
|
||||
type ThreadsJoinedByParent = Record<string, ThreadsJoined>;
|
||||
|
||||
interface ActiveJoinedThreadsStore {
|
||||
getActiveJoinedThreadsForGuild(guildId: string): ThreadsJoinedByParent;
|
||||
}
|
||||
|
||||
const ActiveJoinedThreadsStore: ActiveJoinedThreadsStore = findStoreLazy("ActiveJoinedThreadsStore");
|
||||
|
||||
function onClick() {
|
||||
const channels: Array<any> = [];
|
||||
|
||||
Object.values(GuildStore.getGuilds()).forEach(guild => {
|
||||
GuildChannelStore.getChannels(guild.id).SELECTABLE
|
||||
.concat(GuildChannelStore.getChannels(guild.id).VOCAL)
|
||||
GuildChannelStore.getChannels(guild.id).SELECTABLE // Array<{ channel, comparator }>
|
||||
.concat(GuildChannelStore.getChannels(guild.id).VOCAL) // Array<{ channel, comparator }>
|
||||
.concat(
|
||||
Object.values(ActiveJoinedThreadsStore.getActiveJoinedThreadsForGuild(guild.id))
|
||||
.flatMap(threadChannels => Object.values(threadChannels))
|
||||
)
|
||||
.forEach((c: { channel: { id: string; }; }) => {
|
||||
if (!ReadStateStore.hasUnread(c.channel.id)) return;
|
||||
|
||||
|
|
5
src/plugins/replaceGoogleSearch/README.md
Normal file
5
src/plugins/replaceGoogleSearch/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# ReplaceGoogleSearch
|
||||
|
||||
Replaces the Google search with different Engines
|
||||
|
||||
![Visualization](https://github.com/Vendicated/Vencord/assets/61953774/8b8158d2-0407-4d7b-9dff-a8b9bdc1a122)
|
107
src/plugins/replaceGoogleSearch/index.tsx
Normal file
107
src/plugins/replaceGoogleSearch/index.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Flex, Menu } from "@webpack/common";
|
||||
|
||||
const DefaultEngines = {
|
||||
Google: "https://www.google.com/search?q=",
|
||||
DuckDuckGo: "https://duckduckgo.com/",
|
||||
Bing: "https://www.bing.com/search?q=",
|
||||
Yahoo: "https://search.yahoo.com/search?p=",
|
||||
GitHub: "https://github.com/search?q=",
|
||||
Kagi: "https://kagi.com/search?q=",
|
||||
Yandex: "https://yandex.com/search/?text=",
|
||||
AOL: "https://search.aol.com/aol/search?q=",
|
||||
Baidu: "https://www.baidu.com/s?wd=",
|
||||
Wikipedia: "https://wikipedia.org/w/index.php?search=",
|
||||
} as const;
|
||||
|
||||
const settings = definePluginSettings({
|
||||
customEngineName: {
|
||||
description: "Name of the custom search engine",
|
||||
type: OptionType.STRING,
|
||||
placeholder: "Google"
|
||||
},
|
||||
customEngineURL: {
|
||||
description: "The URL of your Engine",
|
||||
type: OptionType.STRING,
|
||||
placeholder: "https://google.com/search?q="
|
||||
}
|
||||
});
|
||||
|
||||
function search(src: string, engine: string) {
|
||||
open(engine + encodeURIComponent(src), "_blank");
|
||||
}
|
||||
|
||||
function makeSearchItem(src: string) {
|
||||
let Engines = {};
|
||||
|
||||
if (settings.store.customEngineName && settings.store.customEngineURL) {
|
||||
Engines[settings.store.customEngineName] = settings.store.customEngineURL;
|
||||
}
|
||||
|
||||
Engines = { ...Engines, ...DefaultEngines };
|
||||
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
label="Search Text"
|
||||
key="search-text"
|
||||
id="vc-search-text"
|
||||
>
|
||||
{Object.keys(Engines).map((engine, i) => {
|
||||
const key = "vc-search-content-" + engine;
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
key={key}
|
||||
id={key}
|
||||
label={
|
||||
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
||||
<img
|
||||
style={{
|
||||
borderRadius: "50%"
|
||||
}}
|
||||
aria-hidden="true"
|
||||
height={16}
|
||||
width={16}
|
||||
src={`https://www.google.com/s2/favicons?domain=${Engines[engine]}`}
|
||||
/>
|
||||
{engine}
|
||||
</Flex>
|
||||
}
|
||||
action={() => search(src, Engines[engine])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, _props) => {
|
||||
const selection = document.getSelection()?.toString();
|
||||
if (!selection) return;
|
||||
|
||||
const group = findGroupChildrenByChildId("search-google", children);
|
||||
if (group) {
|
||||
const idx = group.findIndex(c => c?.props?.id === "search-google");
|
||||
if (idx !== -1) group[idx] = makeSearchItem(selection);
|
||||
}
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "ReplaceGoogleSearch",
|
||||
description: "Replaces the Google search with different Engines",
|
||||
authors: [Devs.Moxxie, Devs.Ethan],
|
||||
|
||||
settings,
|
||||
|
||||
contextMenus: {
|
||||
"message": messageContextMenuPatch
|
||||
}
|
||||
});
|
|
@ -135,7 +135,7 @@ export default definePlugin({
|
|||
find: '"MessageActionCreators"',
|
||||
replacement: {
|
||||
match: /(?<=focusMessage\(\i\){.+?)(?=focus:{messageId:(\i)})/,
|
||||
replace: "before:$1,"
|
||||
replace: "after:$1,"
|
||||
}
|
||||
},
|
||||
// Force Server Home instead of Server Guide
|
||||
|
|
|
@ -20,7 +20,7 @@ import "./style.css";
|
|||
|
||||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import ExpandableHeader from "@components/ExpandableHeader";
|
||||
import { ExpandableHeader } from "@components/ExpandableHeader";
|
||||
import { OpenExternalIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
|
|
@ -436,7 +436,7 @@ export default definePlugin({
|
|||
},
|
||||
},
|
||||
{
|
||||
find: ".shouldCloseDefaultModals",
|
||||
find: 'className:"channelMention",children',
|
||||
replacement: {
|
||||
// Show inside voice channel instead of trying to join them when clicking on a channel mention
|
||||
match: /(?<=getChannel\(\i\);if\(null!=(\i))(?=.{0,100}?selectVoiceChannel)/,
|
||||
|
|
|
@ -80,11 +80,19 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: "auto_removed:",
|
||||
find: "prod_discoverable_guilds",
|
||||
predicate: () => settings.store.disableDiscoveryFilters,
|
||||
replacement: {
|
||||
match: /filters:\i\.join\(" AND "\),facets:\[/,
|
||||
replace: "facets:["
|
||||
match: /\{"auto_removed:.*?\}/,
|
||||
replace: "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "MINIMUM_MEMBER_COUNT:",
|
||||
predicate: () => settings.store.disableDiscoveryFilters,
|
||||
replacement: {
|
||||
match: /MINIMUM_MEMBER_COUNT:function\(\)\{return \i}/,
|
||||
replace: "MINIMUM_MEMBER_COUNT:() => \">0\""
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -9,11 +9,11 @@ import "./styles.css";
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findComponentLazy } from "@webpack";
|
||||
import { ChannelStore, Forms, GuildMemberStore, i18n, Text, Tooltip } from "@webpack/common";
|
||||
import { ChannelStore, GuildMemberStore, i18n, Text, Tooltip } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
import { FunctionComponent, ReactNode } from "react";
|
||||
|
||||
const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER"));
|
||||
|
||||
|
@ -26,7 +26,6 @@ const settings = definePluginSettings({
|
|||
displayStyle: {
|
||||
description: "How to display the timeout duration",
|
||||
type: OptionType.SELECT,
|
||||
restartNeeded: true,
|
||||
options: [
|
||||
{ label: "In the Tooltip", value: DisplayStyle.Tooltip },
|
||||
{ label: "Next to the timeout icon", value: DisplayStyle.Inline, default: true },
|
||||
|
@ -60,7 +59,7 @@ function renderTimeout(message: Message, inline: boolean) {
|
|||
export default definePlugin({
|
||||
name: "ShowTimeoutDuration",
|
||||
description: "Shows how much longer a user's timeout will last, either in the timeout icon tooltip or next to it",
|
||||
authors: [Devs.Ven],
|
||||
authors: [Devs.Ven, Devs.Sqaaakoi],
|
||||
|
||||
settings,
|
||||
|
||||
|
@ -70,33 +69,20 @@ export default definePlugin({
|
|||
replacement: [
|
||||
{
|
||||
match: /(\i)\.Tooltip,{(text:.{0,30}\.Messages\.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY)/,
|
||||
get replace() {
|
||||
if (settings.store.displayStyle === DisplayStyle.Inline)
|
||||
return "$self.TooltipWrapper,{vcProps:arguments[0],$2";
|
||||
|
||||
return "$1.Tooltip,{text:$self.renderTimeoutDuration(arguments[0])";
|
||||
}
|
||||
replace: "$self.TooltipWrapper,{message:arguments[0].message,$2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
renderTimeoutDuration: ErrorBoundary.wrap(({ message }: { message: Message; }) => {
|
||||
return (
|
||||
<>
|
||||
<Forms.FormText>{i18n.Messages.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}</Forms.FormText>
|
||||
<Forms.FormText className={Margins.top8}>
|
||||
{renderTimeout(message, false)}
|
||||
</Forms.FormText>
|
||||
</>
|
||||
);
|
||||
}, { noop: true }),
|
||||
|
||||
TooltipWrapper: ErrorBoundary.wrap(({ vcProps: { message }, ...tooltipProps }: { vcProps: { message: Message; }; }) => {
|
||||
TooltipWrapper: ErrorBoundary.wrap(({ message, children, text }: { message: Message; children: FunctionComponent<any>; text: ReactNode; }) => {
|
||||
if (settings.store.displayStyle === DisplayStyle.Tooltip) return <Tooltip
|
||||
children={children}
|
||||
text={renderTimeout(message, false)}
|
||||
/>;
|
||||
return (
|
||||
<div className="vc-std-wrapper">
|
||||
<Tooltip {...tooltipProps as any} />
|
||||
|
||||
<Tooltip text={text} children={children} />
|
||||
<Text variant="text-md/normal" color="status-danger">
|
||||
{renderTimeout(message, true)} timeout remaining
|
||||
</Text>
|
||||
|
|
|
@ -2,3 +2,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vc-std-wrapper [class*="communicationDisabled"] {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ export default definePlugin({
|
|||
patches: [{
|
||||
find: "Messages.ACTIVITY_SETTINGS",
|
||||
replacement: {
|
||||
match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\))/,
|
||||
match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\)}\))/,
|
||||
replace: (_, commaOrSemi, settings, elements) => "" +
|
||||
`${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
|
||||
`&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`
|
||||
|
|
|
@ -48,7 +48,7 @@ export default definePlugin({
|
|||
})),
|
||||
{
|
||||
// channel mentions
|
||||
find: ".shouldCloseDefaultModals",
|
||||
find: 'className:"channelMention",children',
|
||||
replacement: {
|
||||
match: /onClick:(\i)(?=,.{0,30}className:"channelMention".+?(\i)\.inContent)/,
|
||||
replace: (_, onClick, props) => ""
|
||||
|
|
|
@ -36,6 +36,10 @@ interface GuildContextProps {
|
|||
guild?: Guild;
|
||||
}
|
||||
|
||||
interface GroupDMContextProps {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
format: {
|
||||
type: OptionType.SELECT,
|
||||
|
@ -145,10 +149,27 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon
|
|||
));
|
||||
};
|
||||
|
||||
const GroupDMContext: NavContextMenuPatchCallback = (children, { channel }: GroupDMContextProps) => {
|
||||
if (!channel) return;
|
||||
|
||||
children.splice(-1, 0, (
|
||||
<Menu.MenuGroup>
|
||||
<Menu.MenuItem
|
||||
id="view-group-channel-icon"
|
||||
label="View Icon"
|
||||
action={() =>
|
||||
openImage(IconUtils.getChannelIconURL(channel)!)
|
||||
}
|
||||
icon={ImageIcon}
|
||||
/>
|
||||
</Menu.MenuGroup>
|
||||
));
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "ViewIcons",
|
||||
authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz],
|
||||
description: "Makes avatars and banners in user profiles clickable, and adds View Icon/Banner entries in the user and server context menu",
|
||||
authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz, Devs.nyx],
|
||||
description: "Makes avatars and banners in user profiles clickable, adds View Icon/Banner entries in the user, server and group channel context menu.",
|
||||
tags: ["ImageUtilities"],
|
||||
|
||||
settings,
|
||||
|
@ -157,11 +178,12 @@ export default definePlugin({
|
|||
|
||||
contextMenus: {
|
||||
"user-context": UserContext,
|
||||
"guild-context": GuildContext
|
||||
"guild-context": GuildContext,
|
||||
"gdm-context": GroupDMContext
|
||||
},
|
||||
|
||||
patches: [
|
||||
// Make pfps clickable
|
||||
// Profiles Modal pfp
|
||||
{
|
||||
find: "User Profile Modal - Context Menu",
|
||||
replacement: {
|
||||
|
@ -169,7 +191,7 @@ export default definePlugin({
|
|||
replace: "{src:$1,onClick:()=>$self.openImage($1)"
|
||||
}
|
||||
},
|
||||
// Make banners clickable
|
||||
// Banners
|
||||
{
|
||||
find: ".NITRO_BANNER,",
|
||||
replacement: {
|
||||
|
@ -180,12 +202,37 @@ export default definePlugin({
|
|||
'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,'
|
||||
}
|
||||
},
|
||||
// User DMs "User Profile" popup in the right
|
||||
{
|
||||
find: ".avatarPositionPanel",
|
||||
replacement: {
|
||||
match: /(?<=avatarWrapperNonUserBot.{0,50})onClick:(\i\|\|\i)\?void 0(?<=,avatarSrc:(\i).+?)/,
|
||||
replace: "style:($1)?{cursor:\"pointer\"}:{},onClick:$1?()=>{$self.openImage($2)}"
|
||||
}
|
||||
},
|
||||
// Group DMs top small & large icon
|
||||
{
|
||||
find: /\.recipients\.length>=2(?!<isMultiUserDM.{0,50})/,
|
||||
replacement: {
|
||||
match: /null==\i\.icon\?.+?src:(\(0,\i\.getChannelIconURL\).+?\))(?=[,}])/,
|
||||
replace: (m, iconUrl) => `${m},onClick:()=>$self.openImage(${iconUrl})`
|
||||
}
|
||||
},
|
||||
// User DMs top small icon
|
||||
{
|
||||
find: ".cursorPointer:null,children",
|
||||
replacement: {
|
||||
match: /.Avatar,.+?src:(.+?\))(?=[,}])/,
|
||||
replace: (m, avatarUrl) => `${m},onClick:()=>$self.openImage(${avatarUrl})`
|
||||
}
|
||||
},
|
||||
// User Dms top large icon
|
||||
{
|
||||
find: 'experimentLocation:"empty_messages"',
|
||||
replacement: {
|
||||
match: /.Avatar,.+?src:(.+?\))(?=[,}])/,
|
||||
replace: (m, avatarUrl) => `${m},onClick:()=>$self.openImage(${avatarUrl})`
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -422,6 +422,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "Av32000",
|
||||
id: 593436735380127770n,
|
||||
},
|
||||
Noxillio: {
|
||||
name: "Noxillio",
|
||||
id: 138616536502894592n,
|
||||
},
|
||||
Kyuuhachi: {
|
||||
name: "Kyuuhachi",
|
||||
id: 236588665420251137n,
|
||||
|
@ -442,6 +446,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "newwares",
|
||||
id: 421405303951851520n
|
||||
},
|
||||
JohnyTheCarrot: {
|
||||
name: "JohnyTheCarrot",
|
||||
id: 132819036282159104n
|
||||
},
|
||||
puv: {
|
||||
name: "puv",
|
||||
id: 469441552251355137n
|
||||
|
@ -490,6 +498,22 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "ScattrdBlade",
|
||||
id: 678007540608532491n
|
||||
},
|
||||
goodbee: {
|
||||
name: "goodbee",
|
||||
id: 658968552606400512n
|
||||
},
|
||||
Moxxie: {
|
||||
name: "Moxxie",
|
||||
id: 712653921692155965n,
|
||||
},
|
||||
Ethan: {
|
||||
name: "Ethan",
|
||||
id: 721717126523781240n,
|
||||
},
|
||||
nyx: {
|
||||
name: "verticalsync",
|
||||
id: 328165170536775680n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
|
@ -23,9 +23,11 @@ export * from "./constants";
|
|||
export * from "./discord";
|
||||
export * from "./guards";
|
||||
export * from "./lazy";
|
||||
export * from "./lazyReact";
|
||||
export * from "./localStorage";
|
||||
export * from "./Logger";
|
||||
export * from "./margins";
|
||||
export * from "./mergeDefaults";
|
||||
export * from "./misc";
|
||||
export * from "./modal";
|
||||
export * from "./onlyOnce";
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PatchReplacement, ReplaceFn } from "./types";
|
||||
import { Patch, PatchReplacement, ReplaceFn } from "./types";
|
||||
|
||||
export function canonicalizeMatch<T extends RegExp | string>(match: T): T {
|
||||
if (typeof match === "string") return match;
|
||||
|
@ -55,3 +55,9 @@ export function canonicalizeReplacement(replacement: Pick<PatchReplacement, "mat
|
|||
);
|
||||
Object.defineProperties(replacement, descriptors);
|
||||
}
|
||||
|
||||
export function canonicalizeFind(patch: Patch) {
|
||||
const descriptors = Object.getOwnPropertyDescriptors(patch);
|
||||
descriptors.find = canonicalizeDescriptor(descriptors.find, canonicalizeMatch);
|
||||
Object.defineProperties(patch, descriptors);
|
||||
}
|
||||
|
|
|
@ -244,7 +244,7 @@ export interface PluginSettingSliderDef {
|
|||
stickToMarkers?: boolean;
|
||||
}
|
||||
|
||||
interface IPluginOptionComponentProps {
|
||||
export interface IPluginOptionComponentProps {
|
||||
/**
|
||||
* Run this when the value changes.
|
||||
*
|
||||
|
|
3
src/webpack/common/types/stores.d.ts
vendored
3
src/webpack/common/types/stores.d.ts
vendored
|
@ -63,7 +63,7 @@ export interface CustomEmoji {
|
|||
originalName?: string;
|
||||
require_colons: boolean;
|
||||
roles: string[];
|
||||
url: string;
|
||||
type: "GUILD_EMOJI";
|
||||
}
|
||||
|
||||
export interface UnicodeEmoji {
|
||||
|
@ -75,6 +75,7 @@ export interface UnicodeEmoji {
|
|||
};
|
||||
index: number;
|
||||
surrogates: string;
|
||||
type: "UNICODE";
|
||||
uniqueName: string;
|
||||
useSpriteSheet: boolean;
|
||||
get allNamesString(): string;
|
||||
|
|
|
@ -138,10 +138,10 @@ waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m);
|
|||
|
||||
export const { Permissions: PermissionsBits } = findLazy(m => typeof m.Permissions?.ADMINISTRATOR === "bigint") as { Permissions: t.PermissionsBits; };
|
||||
|
||||
export const zustandCreate: typeof import("zustand").default = findByCodeLazy("will be removed in v4");
|
||||
export const zustandCreate = findByCodeLazy("will be removed in v4");
|
||||
|
||||
const persistFilter = filters.byCode("[zustand persist middleware]");
|
||||
export const { persist: zustandPersist }: typeof import("zustand/middleware") = findLazy(m => m.persist && persistFilter(m.persist));
|
||||
export const { persist: zustandPersist } = findLazy(m => m.persist && persistFilter(m.persist));
|
||||
|
||||
export const MessageActions = findByPropsLazy("editMessage", "sendMessage");
|
||||
export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal");
|
||||
|
|
|
@ -99,6 +99,16 @@ Object.defineProperty(Function.prototype, "O", {
|
|||
};
|
||||
|
||||
onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded);
|
||||
|
||||
// Returns whether a chunk has been loaded
|
||||
Object.defineProperty(onChunksLoaded, "j", {
|
||||
set(v) {
|
||||
delete onChunksLoaded.j;
|
||||
onChunksLoaded.j = v;
|
||||
originalOnChunksLoaded.j = v;
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(this, "O", {
|
||||
|
@ -122,7 +132,7 @@ Object.defineProperty(Function.prototype, "m", {
|
|||
// When using react devtools or other extensions, we may also catch their webpack here.
|
||||
// This ensures we actually got the right one
|
||||
const { stack } = new Error();
|
||||
if (stack?.includes("discord.com") || stack?.includes("discordapp.com")) {
|
||||
if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(v)) {
|
||||
logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? "");
|
||||
patchFactories(v);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import { makeLazy, proxyLazy } from "@utils/lazy";
|
||||
import { LazyComponent } from "@utils/lazyReact";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { canonicalizeMatch } from "@utils/patches";
|
||||
|
@ -402,7 +402,8 @@ export function findExportedComponentLazy<T extends object = any>(...props: stri
|
|||
});
|
||||
}
|
||||
|
||||
const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\((\[\i\.\i\(".+?"\).+?\])\)|Promise\.resolve\(\)).then\(\i\.bind\(\i,"(.+?)"\)\)/;
|
||||
export const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\))|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/;
|
||||
export const ChunkIdsRegex = /\("(.+?)"\)/g;
|
||||
|
||||
/**
|
||||
* Extract and load chunks using their entry point
|
||||
|
@ -431,7 +432,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
|
|||
return;
|
||||
}
|
||||
|
||||
const [, rawChunkIds, entryPointId] = match;
|
||||
const [, rawChunkIdsArray, rawChunkIdsSingle, entryPointId] = match;
|
||||
if (Number.isNaN(Number(entryPointId))) {
|
||||
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number");
|
||||
logger.warn(err, "Code:", code, "Matcher:", matcher);
|
||||
|
@ -443,8 +444,9 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
|
|||
return;
|
||||
}
|
||||
|
||||
const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
|
||||
if (rawChunkIds) {
|
||||
const chunkIds = Array.from(rawChunkIds.matchAll(/\("(.+?)"\)/g)).map((m: any) => m[1]);
|
||||
const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]);
|
||||
await Promise.all(chunkIds.map(id => wreq.e(id)));
|
||||
}
|
||||
|
||||
|
@ -462,7 +464,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
|
|||
export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtractAndLoadChunksRegex) {
|
||||
if (IS_DEV) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]);
|
||||
|
||||
return () => extractAndLoadChunks(code, matcher);
|
||||
return makeLazy(() => extractAndLoadChunks(code, matcher));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,7 +29,15 @@
|
|||
"@webpack/types": ["./webpack/common/types"],
|
||||
"@webpack/common": ["./webpack/common"],
|
||||
"@webpack": ["./webpack/webpack"]
|
||||
}
|
||||
},
|
||||
|
||||
"plugins": [
|
||||
// Transform paths in output .d.ts files (Include this line if you output declarations files)
|
||||
{
|
||||
"transform": "typescript-transform-paths",
|
||||
"afterDeclarations": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*", "browser/**/*", "scripts/**/*"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue