Compare commits
50 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ed873ef9de | ||
|
d8a553feb0 | ||
|
4717612090 | ||
|
5d1283bd85 | ||
|
3b945b87b8 | ||
|
19c762f9c1 | ||
|
990adf7527 | ||
|
983414d024 | ||
|
d5c05d857f | ||
|
bff6788546 | ||
|
253183a16a | ||
|
0fb3901a18 | ||
|
1b199ec5d8 | ||
|
40395d562a | ||
|
7322c3af04 | ||
|
36c27f1111 | ||
|
95db6c32a3 | ||
|
bed5e98bb0 | ||
|
a5392e5c53 | ||
|
abbd298b31 | ||
|
e219aaa062 | ||
|
cab72e1be6 | ||
|
92372bde1d | ||
|
6747276a87 | ||
|
03915b7533 | ||
|
5e2ec368ad | ||
|
ab8c93fbac | ||
|
d6a3edefd9 | ||
|
727297ec4e | ||
|
eccc4b0be1 | ||
|
8465140bc4 | ||
|
e6ccb751a0 | ||
|
dfc7a15083 | ||
|
37003edae9 | ||
|
faa90eccd3 | ||
|
c91b0df607 | ||
|
f56d99e133 | ||
|
c690662802 | ||
|
4918d699d5 | ||
|
5ec517875e | ||
|
cf56ad985b | ||
|
c09d1558f7 | ||
|
eb190b660e | ||
|
d6f9068695 | ||
|
cb507babaa | ||
|
235d114193 | ||
|
9aba70dcb1 | ||
|
0b61d29c31 | ||
|
335a13a38a | ||
|
128ee41252 |
15
README.md
15
README.md
@ -4,12 +4,14 @@ The cutest Discord client mod
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Super easy to install (one click installer)
|
- Super easy to install (Download Installer, open, click install button, done)
|
||||||
- 90+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
- 100+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
||||||
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
||||||
|
- Fairly lightweight despite the many inbuilt plugins
|
||||||
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
||||||
|
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
|
||||||
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
||||||
- Works in all Electron versions (Confirmed working on versions 13-23)
|
- Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry
|
||||||
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
||||||
|
|
||||||
## Installing / Uninstalling
|
## Installing / Uninstalling
|
||||||
@ -20,7 +22,7 @@ The cutest Discord client mod
|
|||||||
|
|
||||||
[![Get it on the Firefox Webstore](https://blog.mozilla.org/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [![Get it on the Chrome Webstore](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
|
[![Get it on the Firefox Webstore](https://blog.mozilla.org/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [![Get it on the Chrome Webstore](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
|
||||||
|
|
||||||
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS and plugins making use of external resources will not work with the UserScript.
|
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that the CSS Editor, Themes loaded from remote sources and co. will not work in the UserScript. Use the extension if you need any of those
|
||||||
|
|
||||||
## Building from Source
|
## Building from Source
|
||||||
|
|
||||||
@ -39,3 +41,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS
|
|||||||
[join]: https://discord.gg/D9uwnFnqmd
|
[join]: https://discord.gg/D9uwnFnqmd
|
||||||
|
|
||||||
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
Discord is trademark of Discord Inc. and solely mentioned for the sake of descriptivity.
|
||||||
|
Mention of it does not imply any affiliation with or endorsement by Discord Inc.
|
||||||
|
@ -92,6 +92,7 @@ function GM_fetch(url, opt) {
|
|||||||
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
||||||
resp.text = () => blobTo("text", blob);
|
resp.text = () => blobTo("text", blob);
|
||||||
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
||||||
|
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
|
||||||
resolve(resp);
|
resolve(resp);
|
||||||
};
|
};
|
||||||
options.ontimeout = () => reject("fetch timeout");
|
options.ontimeout = () => reject("fetch timeout");
|
||||||
|
@ -31,12 +31,14 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
|
|||||||
|
|
||||||
Install `pnpm`:
|
Install `pnpm`:
|
||||||
|
|
||||||
> :exclamation: this may need to be run as admin depending on your system, and you may need to close and reopen your terminal.
|
> :exclamation: This next command may need to be run as admin/sudo depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm i -g pnpm
|
npm i -g pnpm
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> :exclamation: **IMPORTANT** Make sure you aren't using an admin/root terminal from here onwards. It **will** mess up your Discord/Vencord instance and you **will** most likely have to reinstall.
|
||||||
|
|
||||||
Clone Vencord:
|
Clone Vencord:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.0.7",
|
"version": "1.1.1",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
|
@ -33,6 +33,8 @@ export const banner = {
|
|||||||
`.trim()
|
`.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
|
||||||
|
|
||||||
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
@ -69,9 +71,15 @@ export const globPlugins = {
|
|||||||
const files = await readdir(`./src/${dir}`);
|
const files = await readdir(`./src/${dir}`);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.startsWith(".")) continue;
|
if (file.startsWith(".")) continue;
|
||||||
if (file === "index.ts") {
|
if (file === "index.ts") continue;
|
||||||
continue;
|
const fileBits = file.split(".");
|
||||||
|
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1))) {
|
||||||
|
const mod = fileBits.at(-2);
|
||||||
|
if (mod === "dev" && !watch) continue;
|
||||||
|
if (mod === "web" && !isWeb) continue;
|
||||||
|
if (mod === "desktop" && isWeb) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mod = `p${i}`;
|
const mod = `p${i}`;
|
||||||
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
|
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
|
||||||
plugins += `[${mod}.name]:${mod},\n`;
|
plugins += `[${mod}.name]:${mod},\n`;
|
||||||
|
@ -95,3 +95,12 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
if (!IS_WEB && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.head.append(Object.assign(document.createElement("style"), {
|
||||||
|
id: "vencord-native-titlebar-style",
|
||||||
|
textContent: "[class*=titleBar-]{display: none!important}"
|
||||||
|
}));
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
141
src/api/ContextMenu.ts
Normal file
141
src/api/ContextMenu.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* 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 Logger from "@utils/Logger";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param children The rendered context menu elements
|
||||||
|
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
||||||
|
*/
|
||||||
|
export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, args?: Array<any>) => void;
|
||||||
|
/**
|
||||||
|
* @param The navId of the context menu being patched
|
||||||
|
* @param children The rendered context menu elements
|
||||||
|
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
||||||
|
*/
|
||||||
|
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, args?: Array<any>) => void;
|
||||||
|
|
||||||
|
const ContextMenuLogger = new Logger("ContextMenu");
|
||||||
|
|
||||||
|
export const navPatches = new Map<string, Set<NavContextMenuPatchCallback>>();
|
||||||
|
export const globalPatches = new Set<GlobalContextMenuPatchCallback>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a context menu patch
|
||||||
|
* @param navId The navId(s) for the context menu(s) to patch
|
||||||
|
* @param patch The patch to be applied
|
||||||
|
*/
|
||||||
|
export function addContextMenuPatch(navId: string | Array<string>, patch: NavContextMenuPatchCallback) {
|
||||||
|
if (!Array.isArray(navId)) navId = [navId];
|
||||||
|
for (const id of navId) {
|
||||||
|
let contextMenuPatches = navPatches.get(id);
|
||||||
|
if (!contextMenuPatches) {
|
||||||
|
contextMenuPatches = new Set();
|
||||||
|
navPatches.set(id, contextMenuPatches);
|
||||||
|
}
|
||||||
|
|
||||||
|
contextMenuPatches.add(patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a global context menu patch that fires the patch for all context menus
|
||||||
|
* @param patch The patch to be applied
|
||||||
|
*/
|
||||||
|
export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback) {
|
||||||
|
globalPatches.add(patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a context menu patch
|
||||||
|
* @param navId The navId(s) for the context menu(s) to remove the patch
|
||||||
|
* @param patch The patch to be removed
|
||||||
|
* @returns Wheter the patch was sucessfully removed from the context menu(s)
|
||||||
|
*/
|
||||||
|
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
|
||||||
|
const navIds = Array.isArray(navId) ? navId : [navId as string];
|
||||||
|
|
||||||
|
const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false);
|
||||||
|
|
||||||
|
return (Array.isArray(navId) ? results : results[0]) as T extends string ? boolean : Array<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a global context menu patch
|
||||||
|
* @returns Wheter the patch was sucessfully removed
|
||||||
|
*/
|
||||||
|
export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {
|
||||||
|
return globalPatches.delete(patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper function for finding the children array of a group nested inside a context menu based on the id of one of its childs
|
||||||
|
* @param id The id of the child
|
||||||
|
*/
|
||||||
|
export function findGroupChildrenByChildId(id: string, children: Array<React.ReactElement>, itemsArray?: Array<React.ReactElement>): Array<React.ReactElement> | null {
|
||||||
|
for (const child of children) {
|
||||||
|
if (child == null) continue;
|
||||||
|
|
||||||
|
if (child.props?.id === id) return itemsArray ?? null;
|
||||||
|
|
||||||
|
let nextChildren = child.props?.children;
|
||||||
|
if (nextChildren) {
|
||||||
|
if (!Array.isArray(nextChildren)) {
|
||||||
|
nextChildren = [nextChildren];
|
||||||
|
child.props.children = nextChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
|
||||||
|
if (found !== null) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
contextMenuApiArguments?: Array<any>;
|
||||||
|
navId: string;
|
||||||
|
children: Array<ReactElement>;
|
||||||
|
"aria-label": string;
|
||||||
|
onSelect: (() => void) | undefined;
|
||||||
|
onClose: (callback: (...args: Array<any>) => any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function _patchContextMenu(props: ContextMenuProps) {
|
||||||
|
const contextMenuPatches = navPatches.get(props.navId);
|
||||||
|
|
||||||
|
if (contextMenuPatches) {
|
||||||
|
for (const patch of contextMenuPatches) {
|
||||||
|
try {
|
||||||
|
patch(props.children, props.contextMenuApiArguments);
|
||||||
|
} catch (err) {
|
||||||
|
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const patch of globalPatches) {
|
||||||
|
try {
|
||||||
|
patch(props.navId, props.children, props.contextMenuApiArguments);
|
||||||
|
} catch (err) {
|
||||||
|
ContextMenuLogger.error("Global patch errored,", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ import "./styles.css";
|
|||||||
|
|
||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
||||||
|
|
||||||
import { NotificationData } from "./Notifications";
|
import { NotificationData } from "./Notifications";
|
||||||
|
|
||||||
@ -32,7 +32,8 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
icon,
|
icon,
|
||||||
onClick,
|
onClick,
|
||||||
onClose,
|
onClose,
|
||||||
image
|
image,
|
||||||
|
permanent
|
||||||
}: NotificationData) {
|
}: NotificationData) {
|
||||||
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
||||||
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
||||||
@ -43,7 +44,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
|
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isHover || !hasFocus || timeout === 0) return void setElapsed(0);
|
if (isHover || !hasFocus || timeout === 0 || permanent) return void setElapsed(0);
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
const elapsed = Date.now() - start;
|
const elapsed = Date.now() - start;
|
||||||
@ -74,14 +75,36 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
<div className="vc-notification">
|
<div className="vc-notification">
|
||||||
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
|
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
|
||||||
<div className="vc-notification-content">
|
<div className="vc-notification-content">
|
||||||
<Forms.FormTitle tag="h2">{title}</Forms.FormTitle>
|
<div className="vc-notification-header">
|
||||||
|
<h2 className="vc-notification-title">{title}</h2>
|
||||||
|
<button
|
||||||
|
style={{ all: "unset", cursor: "pointer" }}
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onClose!();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="vc-notification-close-btn"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
role="img"
|
||||||
|
aria-labelledby="vc-notification-dismiss-title"
|
||||||
|
>
|
||||||
|
<title id="vc-notification-dismiss-title">Dismiss Notification</title>
|
||||||
|
<path fill="currentColor" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{image && <img className="vc-notification-img" src={image} alt="" />}
|
{image && <img className="vc-notification-img" src={image} alt="" />}
|
||||||
{timeout !== 0 && (
|
{timeout !== 0 && !permanent && (
|
||||||
<div
|
<div
|
||||||
className="vc-notification-progressbar"
|
className="vc-notification-progressbar"
|
||||||
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
||||||
@ -89,4 +112,6 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
}, {
|
||||||
|
onError: ({ props }) => props.onClose!()
|
||||||
});
|
});
|
||||||
|
@ -54,6 +54,8 @@ export interface NotificationData {
|
|||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
onClose?(): void;
|
onClose?(): void;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
/** Whether this notification should not have a timeout */
|
||||||
|
permanent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _showNotification(notification: NotificationData, id: number) {
|
function _showNotification(notification: NotificationData, id: number) {
|
||||||
|
@ -22,17 +22,40 @@
|
|||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vc-notification-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-title {
|
||||||
|
color: var(--header-primary);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-close-btn {
|
||||||
|
color: var(--interactive-normal);
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-close-btn:hover {
|
||||||
|
color: var(--interactive-hover);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.vc-notification-icon {
|
.vc-notification-icon {
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Discord adding 3km margin to generic tags */
|
|
||||||
.vc-notification h2 {
|
|
||||||
margin: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-progressbar {
|
.vc-notification-progressbar {
|
||||||
height: 0.25rem;
|
height: 0.25rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import * as $Badges from "./Badges";
|
import * as $Badges from "./Badges";
|
||||||
import * as $Commands from "./Commands";
|
import * as $Commands from "./Commands";
|
||||||
|
import * as $ContextMenu from "./ContextMenu";
|
||||||
import * as $DataStore from "./DataStore";
|
import * as $DataStore from "./DataStore";
|
||||||
import * as $MemberListDecorators from "./MemberListDecorators";
|
import * as $MemberListDecorators from "./MemberListDecorators";
|
||||||
import * as $MessageAccessories from "./MessageAccessories";
|
import * as $MessageAccessories from "./MessageAccessories";
|
||||||
@ -93,3 +94,8 @@ export const Styles = $Styles;
|
|||||||
* An API allowing you to display notifications
|
* An API allowing you to display notifications
|
||||||
*/
|
*/
|
||||||
export const Notifications = $Notifications;
|
export const Notifications = $Notifications;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An api allowing you to patch and add/remove items to/from context menus
|
||||||
|
*/
|
||||||
|
export const ContextMenu = $ContextMenu;
|
||||||
|
@ -34,6 +34,7 @@ export interface Settings {
|
|||||||
frameless: boolean;
|
frameless: boolean;
|
||||||
transparent: boolean;
|
transparent: boolean;
|
||||||
winCtrlQ: boolean;
|
winCtrlQ: boolean;
|
||||||
|
winNativeTitleBar: boolean;
|
||||||
plugins: {
|
plugins: {
|
||||||
[plugin: string]: {
|
[plugin: string]: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -57,6 +58,7 @@ const DefaultSettings: Settings = {
|
|||||||
frameless: false,
|
frameless: false,
|
||||||
transparent: false,
|
transparent: false,
|
||||||
winCtrlQ: false,
|
winCtrlQ: false,
|
||||||
|
winNativeTitleBar: false,
|
||||||
plugins: {},
|
plugins: {},
|
||||||
|
|
||||||
notifications: {
|
notifications: {
|
||||||
@ -90,7 +92,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
|||||||
// Return empty for plugins with no settings
|
// Return empty for plugins with no settings
|
||||||
if (path === "plugins" && p in plugins)
|
if (path === "plugins" && p in plugins)
|
||||||
return target[p] = makeProxy({
|
return target[p] = makeProxy({
|
||||||
enabled: plugins[p].required ?? false
|
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
|
||||||
}, root, `plugins.${p}`);
|
}, root, `plugins.${p}`);
|
||||||
|
|
||||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||||
@ -165,11 +167,11 @@ export const Settings = makeProxy(settings);
|
|||||||
* @returns Settings
|
* @returns Settings
|
||||||
*/
|
*/
|
||||||
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
||||||
export function useSettings(paths?: string[]) {
|
export function useSettings(paths?: UseSettings<Settings>[]) {
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
const onUpdate: SubscriptionCallback = paths
|
const onUpdate: SubscriptionCallback = paths
|
||||||
? (value, path) => paths.includes(path) && forceUpdate()
|
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
|
||||||
: forceUpdate;
|
: forceUpdate;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -227,7 +229,7 @@ export function definePluginSettings<D extends SettingsDefinition, C extends Set
|
|||||||
return Settings.plugins[definedSettings.pluginName] as any;
|
return Settings.plugins[definedSettings.pluginName] as any;
|
||||||
},
|
},
|
||||||
use: settings => useSettings(
|
use: settings => useSettings(
|
||||||
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`)
|
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
|
||||||
).plugins[definedSettings.pluginName] as any,
|
).plugins[definedSettings.pluginName] as any,
|
||||||
def,
|
def,
|
||||||
checks: checks ?? {},
|
checks: checks ?? {},
|
||||||
@ -235,3 +237,15 @@ export function definePluginSettings<D extends SettingsDefinition, C extends Set
|
|||||||
};
|
};
|
||||||
return definedSettings;
|
return definedSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UseSettings<T extends object> = ResolveUseSettings<T>[keyof T];
|
||||||
|
|
||||||
|
type ResolveUseSettings<T extends object> = {
|
||||||
|
[Key in keyof T]:
|
||||||
|
Key extends string
|
||||||
|
? T[Key] extends Record<string, unknown>
|
||||||
|
// @ts-ignore "Type instantiation is excessively deep and possibly infinite"
|
||||||
|
? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never
|
||||||
|
: Key
|
||||||
|
: never;
|
||||||
|
};
|
||||||
|
@ -17,20 +17,24 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { LazyComponent } from "@utils/misc";
|
import { LazyComponent } from "@utils/misc";
|
||||||
import { Margins, React } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
import { ErrorCard } from "./ErrorCard";
|
import { ErrorCard } from "./ErrorCard";
|
||||||
|
|
||||||
interface Props {
|
interface Props<T = any> {
|
||||||
/** Render nothing if an error occurs */
|
/** Render nothing if an error occurs */
|
||||||
noop?: boolean;
|
noop?: boolean;
|
||||||
/** Fallback component to render if an error occurs */
|
/** Fallback component to render if an error occurs */
|
||||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
||||||
/** called when an error occurs */
|
/** called when an error occurs. The props property is only available if using .wrap */
|
||||||
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
|
||||||
/** Custom error message */
|
/** Custom error message */
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
||||||
|
/** The props passed to the wrapped component. Only used by wrap */
|
||||||
|
wrappedProps?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = "#e78284";
|
const color = "#e78284";
|
||||||
@ -65,7 +69,7 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
this.props.onError?.(error, errorInfo);
|
this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });
|
||||||
logger.error("A component threw an Error\n", error);
|
logger.error("A component threw an Error\n", error);
|
||||||
logger.error("Component Stack", errorInfo.componentStack);
|
logger.error("Component Stack", errorInfo.componentStack);
|
||||||
}
|
}
|
||||||
@ -84,15 +88,13 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorCard style={{
|
<ErrorCard style={{ overflow: "hidden" }}>
|
||||||
overflow: "hidden",
|
|
||||||
}}>
|
|
||||||
<h1>Oh no!</h1>
|
<h1>Oh no!</h1>
|
||||||
<p>{msg}</p>
|
<p>{msg}</p>
|
||||||
<code>
|
<code>
|
||||||
{this.state.message}
|
{this.state.message}
|
||||||
{!!this.state.stack && (
|
{!!this.state.stack && (
|
||||||
<pre className={Margins.marginTop8}>
|
<pre className={Margins.top8}>
|
||||||
{this.state.stack}
|
{this.state.stack}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@ -103,11 +105,11 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
};
|
};
|
||||||
}) as
|
}) as
|
||||||
React.ComponentType<React.PropsWithChildren<Props>> & {
|
React.ComponentType<React.PropsWithChildren<Props>> & {
|
||||||
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
|
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.ComponentType<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
||||||
<ErrorBoundary {...errorBoundaryProps}>
|
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
|
||||||
<Component {...props} />
|
<Component {...props} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
7
src/components/ErrorCard.css
Normal file
7
src/components/ErrorCard.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.vc-error-card {
|
||||||
|
padding: 2em;
|
||||||
|
background-color: #e7828430;
|
||||||
|
border: 1px solid #e78284;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--text-normal, white);
|
||||||
|
}
|
@ -16,24 +16,15 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Card } from "@webpack/common";
|
import "./ErrorCard.css";
|
||||||
|
|
||||||
interface Props {
|
import { classes } from "@utils/misc";
|
||||||
style?: React.CSSProperties;
|
import type { HTMLProps } from "react";
|
||||||
className?: string;
|
|
||||||
}
|
export function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {
|
||||||
export function ErrorCard(props: React.PropsWithChildren<Props>) {
|
|
||||||
return (
|
return (
|
||||||
<Card className={props.className} style={
|
<div {...props} className={classes(props.className, "vc-error-card")}>
|
||||||
{
|
|
||||||
padding: "2em",
|
|
||||||
backgroundColor: "#e7828430",
|
|
||||||
borderColor: "#e78284",
|
|
||||||
color: "var(--text-normal)",
|
|
||||||
...props.style
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { makeCodeblock } from "@utils/misc";
|
import { makeCodeblock } from "@utils/misc";
|
||||||
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
|
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
|
||||||
import { search } from "@webpack";
|
import { search } from "@webpack";
|
||||||
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
import { Button, Clipboard, Forms, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
import { CheckedTextInput } from "./CheckedTextInput";
|
import { CheckedTextInput } from "./CheckedTextInput";
|
||||||
import ErrorBoundary from "./ErrorBoundary";
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
@ -128,7 +129,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!!diff?.length && (
|
{!!diff?.length && (
|
||||||
<Button className={Margins.marginTop20} onClick={() => {
|
<Button className={Margins.top20} onClick={() => {
|
||||||
try {
|
try {
|
||||||
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
||||||
setCompileResult([true, "Compiled successfully"]);
|
setCompileResult([true, "Compiled successfully"]);
|
||||||
@ -202,7 +203,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
className={Margins.marginTop8}
|
className={Margins.top8}
|
||||||
value={isFunc}
|
value={isFunc}
|
||||||
onChange={setIsFunc}
|
onChange={setIsFunc}
|
||||||
note="'replacement' will be evaled if this is toggled"
|
note="'replacement' will be evaled if this is toggled"
|
||||||
@ -256,7 +257,7 @@ function PatchHelper() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text>
|
<Text variant="heading-md/normal" tag="h2" className={Margins.bottom8}>Patch Helper</Text>
|
||||||
<Forms.FormTitle>find</Forms.FormTitle>
|
<Forms.FormTitle>find</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
@ -296,7 +297,7 @@ function PatchHelper() {
|
|||||||
|
|
||||||
{!!(find && match && replacement) && (
|
{!!(find && match && replacement) && (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.marginTop20}>Code</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
||||||
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
||||||
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
||||||
</>
|
</>
|
||||||
|
@ -30,11 +30,12 @@ import PluginModal from "@components/PluginSettings/PluginModal";
|
|||||||
import { Switch } from "@components/Switch";
|
import { Switch } from "@components/Switch";
|
||||||
import { ChangeList } from "@utils/ChangeList";
|
import { ChangeList } from "@utils/ChangeList";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
|
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
|
||||||
import { openModalLazy } from "@utils/modal";
|
import { openModalLazy } from "@utils/modal";
|
||||||
import { Plugin } from "@utils/types";
|
import { Plugin } from "@utils/types";
|
||||||
import { findByCode, findByPropsLazy } from "@webpack";
|
import { findByCode, findByPropsLazy } from "@webpack";
|
||||||
import { Alerts, Button, Card, Forms, Margins, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
import { Alerts, Button, Card, Forms, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
@ -92,7 +93,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
||||||
const settings = useSettings([`plugins.${plugin.name}`]).plugins[plugin.name];
|
const settings = useSettings([`plugins.${plugin.name}.enabled`]).plugins[plugin.name];
|
||||||
|
|
||||||
const isEnabled = () => settings.enabled ?? false;
|
const isEnabled = () => settings.enabled ?? false;
|
||||||
|
|
||||||
@ -296,15 +297,15 @@ export default ErrorBoundary.wrap(function PluginSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection className={Margins.marginTop16}>
|
<Forms.FormSection className={Margins.top16}>
|
||||||
<ReloadRequiredCard required={changes.hasChanges} />
|
<ReloadRequiredCard required={changes.hasChanges} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
||||||
Filters
|
Filters
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
|
|
||||||
<div className={cl("filter-controls")}>
|
<div className={cl("filter-controls")}>
|
||||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.marginBottom20} />
|
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
|
||||||
<div className={InputStyles.inputWrapper}>
|
<div className={InputStyles.inputWrapper}>
|
||||||
<Select
|
<Select
|
||||||
className={InputStyles.inputDefault}
|
className={InputStyles.inputDefault}
|
||||||
@ -321,15 +322,15 @@ export default ErrorBoundary.wrap(function PluginSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
|
||||||
|
|
||||||
<div className={cl("grid")}>
|
<div className={cl("grid")}>
|
||||||
{plugins}
|
{plugins}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Forms.FormDivider className={Margins.marginTop20} />
|
<Forms.FormDivider className={Margins.top20} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
||||||
Required Plugins
|
Required Plugins
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
<div className={cl("grid")}>
|
<div className={cl("grid")}>
|
||||||
|
@ -18,25 +18,26 @@
|
|||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||||
import { Button, Card, Forms, Margins, Text } from "@webpack/common";
|
import { Button, Card, Forms, Text } from "@webpack/common";
|
||||||
|
|
||||||
function BackupRestoreTab() {
|
function BackupRestoreTab() {
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection title="Settings Sync" className={Margins.marginTop16}>
|
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
|
||||||
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||||
<Flex flexDirection="column">
|
<Flex flexDirection="column">
|
||||||
<strong>Warning</strong>
|
<strong>Warning</strong>
|
||||||
<span>Importing a settings file will overwrite your current settings.</span>
|
<span>Importing a settings file will overwrite your current settings.</span>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
<Text variant="text-md/normal" className={Margins.bottom8}>
|
||||||
You can import and export your Vencord settings as a JSON file.
|
You can import and export your Vencord settings as a JSON file.
|
||||||
This allows you to easily transfer your settings to another device,
|
This allows you to easily transfer your settings to another device,
|
||||||
or recover your settings after reinstalling Vencord or Discord.
|
or recover your settings after reinstalling Vencord or Discord.
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
<Text variant="text-md/normal" className={Margins.bottom8}>
|
||||||
Settings Export contains:
|
Settings Export contains:
|
||||||
<ul>
|
<ul>
|
||||||
<li>— Custom QuickCSS</li>
|
<li>— Custom QuickCSS</li>
|
||||||
|
@ -19,9 +19,10 @@
|
|||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { useAwaiter } from "@utils/misc";
|
import { useAwaiter } from "@utils/misc";
|
||||||
import { findLazy } from "@webpack";
|
import { findLazy } from "@webpack";
|
||||||
import { Card, Forms, Margins, React, TextArea } from "@webpack/common";
|
import { Card, Forms, React, TextArea } from "@webpack/common";
|
||||||
|
|
||||||
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.marginTop20} tag="h5">Validator</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
|
||||||
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
||||||
<div>
|
<div>
|
||||||
{themeLinks.map(link => (
|
{themeLinks.map(link => (
|
||||||
@ -93,7 +94,7 @@ export default ErrorBoundary.wrap(function () {
|
|||||||
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
||||||
<Forms.FormText>One link per line</Forms.FormText>
|
<Forms.FormText>One link per line</Forms.FormText>
|
||||||
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
||||||
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
|
||||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||||
<div style={{ marginBottom: ".5em" }}>
|
<div style={{ marginBottom: ".5em" }}>
|
||||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||||
|
@ -22,9 +22,10 @@ import { ErrorCard } from "@components/ErrorCard";
|
|||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { classes, useAwaiter } from "@utils/misc";
|
import { classes, useAwaiter } from "@utils/misc";
|
||||||
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
||||||
import { Alerts, Button, Card, Forms, Margins, Parser, React, Switch, Toasts } from "@webpack/common";
|
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
@ -109,14 +110,14 @@ function Updatable(props: CommonProps) {
|
|||||||
</ErrorCard>
|
</ErrorCard>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Forms.FormText className={Margins.marginBottom8}>
|
<Forms.FormText className={Margins.bottom8}>
|
||||||
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isOutdated && <Changes updates={updates} {...props} />}
|
{isOutdated && <Changes updates={updates} {...props} />}
|
||||||
|
|
||||||
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}>
|
<Flex className={classes(Margins.bottom8, Margins.top8)}>
|
||||||
{isOutdated && <Button
|
{isOutdated && <Button
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
disabled={isUpdating || isChecking}
|
disabled={isUpdating || isChecking}
|
||||||
@ -175,7 +176,7 @@ function Updatable(props: CommonProps) {
|
|||||||
function Newer(props: CommonProps) {
|
function Newer(props: CommonProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormText className={Margins.marginBottom8}>
|
<Forms.FormText className={Margins.bottom8}>
|
||||||
Your local copy has more recent commits. Please stash or reset them.
|
Your local copy has more recent commits. Please stash or reset them.
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Changes {...props} updates={changes} />
|
<Changes {...props} updates={changes} />
|
||||||
@ -199,7 +200,7 @@ function Updater() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection className={Margins.marginTop16}>
|
<Forms.FormSection className={Margins.top16}>
|
||||||
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.notifyAboutUpdates}
|
value={settings.notifyAboutUpdates}
|
||||||
@ -225,7 +226,7 @@ function Updater() {
|
|||||||
</Link>
|
</Link>
|
||||||
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
|
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
|
||||||
|
|
||||||
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||||
|
|
||||||
|
@ -63,11 +63,15 @@ function VencordSettings() {
|
|||||||
title: "Enable React Developer Tools",
|
title: "Enable React Developer Tools",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
},
|
},
|
||||||
!IS_WEB && !isWindows && {
|
!IS_WEB && (!isWindows ? {
|
||||||
key: "frameless",
|
key: "frameless",
|
||||||
title: "Disable the window frame",
|
title: "Disable the window frame",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
},
|
} : {
|
||||||
|
key: "winNativeTitleBar",
|
||||||
|
title: "Use Windows' native title bar instead of Discord's custom one",
|
||||||
|
note: "Requires a full restart"
|
||||||
|
}),
|
||||||
!IS_WEB && {
|
!IS_WEB && {
|
||||||
key: "transparent",
|
key: "transparent",
|
||||||
title: "Enable window transparency",
|
title: "Enable window transparency",
|
||||||
|
@ -20,6 +20,7 @@ import "./settingsStyles.css";
|
|||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||||
import { findByCodeLazy } from "@webpack";
|
import { findByCodeLazy } from "@webpack";
|
||||||
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
||||||
|
|
||||||
@ -61,8 +62,8 @@ function Settings(props: SettingsProps) {
|
|||||||
<Text variant="heading-md/normal" tag="h2">Vencord Settings</Text>
|
<Text variant="heading-md/normal" tag="h2">Vencord Settings</Text>
|
||||||
|
|
||||||
<TabBar
|
<TabBar
|
||||||
type={TabBar.Types.TOP}
|
type="top"
|
||||||
look={TabBar.Looks.BRAND}
|
look="brand"
|
||||||
className={cl("tab-bar")}
|
className={cl("tab-bar")}
|
||||||
selectedItem={tab}
|
selectedItem={tab}
|
||||||
onItemSelect={SettingsRouter.open}
|
onItemSelect={SettingsRouter.open}
|
||||||
@ -83,7 +84,7 @@ function Settings(props: SettingsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function (props: SettingsProps) {
|
export default function (props: SettingsProps) {
|
||||||
return <ErrorBoundary>
|
return <ErrorBoundary onError={handleComponentFailed}>
|
||||||
<Settings tab={props.tab} />
|
<Settings tab={props.tab} />
|
||||||
</ErrorBoundary>;
|
</ErrorBoundary>;
|
||||||
}
|
}
|
||||||
|
@ -16,29 +16,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isOutdated, rebuild, update } from "@utils/updater";
|
import { maybePromptToUpdate } from "@utils/updater";
|
||||||
|
|
||||||
export async function handleComponentFailed() {
|
export function handleComponentFailed() {
|
||||||
if (isOutdated) {
|
maybePromptToUpdate(
|
||||||
setImmediate(async () => {
|
"Uh Oh! Failed to render this Page." +
|
||||||
const wantsUpdate = confirm(
|
" However, there is an update available that might fix it." +
|
||||||
"Uh Oh! Failed to render this Page." +
|
" Would you like to update and restart now?"
|
||||||
" However, there is an update available that might fix it." +
|
);
|
||||||
" Would you like to update and restart now?"
|
|
||||||
);
|
|
||||||
if (wantsUpdate) {
|
|
||||||
try {
|
|
||||||
await update();
|
|
||||||
await rebuild();
|
|
||||||
if (IS_WEB)
|
|
||||||
location.reload();
|
|
||||||
else
|
|
||||||
DiscordNative.app.relaunch();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
alert("That also failed :( Try updating or reinstalling with the installer!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
3
src/globals.d.ts
vendored
3
src/globals.d.ts
vendored
@ -51,8 +51,7 @@ declare global {
|
|||||||
* Only available when running in Electron, undefined on web.
|
* Only available when running in Electron, undefined on web.
|
||||||
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
|
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
|
||||||
*
|
*
|
||||||
* If you really must use it, mark your plugin as Desktop App only via
|
* If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
|
||||||
* `target: "DESKTOP"`
|
|
||||||
*/
|
*/
|
||||||
export var DiscordNative: any;
|
export var DiscordNative: any;
|
||||||
|
|
||||||
|
@ -79,7 +79,10 @@ if (!process.argv.includes("--vanilla")) {
|
|||||||
options.webPreferences.sandbox = false;
|
options.webPreferences.sandbox = false;
|
||||||
if (settings.frameless) {
|
if (settings.frameless) {
|
||||||
options.frame = false;
|
options.frame = false;
|
||||||
|
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
||||||
|
delete options.frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.transparent) {
|
if (settings.transparent) {
|
||||||
options.transparent = true;
|
options.transparent = true;
|
||||||
options.backgroundColor = "#00000000";
|
options.backgroundColor = "#00000000";
|
||||||
|
@ -24,9 +24,10 @@ import { Heart } from "@components/Heart";
|
|||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { closeModal, Modals, openModal } from "@utils/modal";
|
import { closeModal, Modals, openModal } from "@utils/modal";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Forms, Margins } from "@webpack/common";
|
import { Forms } from "@webpack/common";
|
||||||
|
|
||||||
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
|
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
|
||||||
|
|
||||||
@ -150,7 +151,7 @@ export default definePlugin({
|
|||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
This Badge is a special perk for Vencord Donors
|
This Badge is a special perk for Vencord Donors
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Forms.FormText className={Margins.marginTop20}>
|
<Forms.FormText className={Margins.top20}>
|
||||||
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
</div>
|
</div>
|
||||||
|
69
src/plugins/apiContextMenu.ts
Normal file
69
src/plugins/apiContextMenu.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Settings } from "@api/settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { addListener, removeListener } from "@webpack";
|
||||||
|
|
||||||
|
function listener(exports: any, id: number) {
|
||||||
|
if (!Settings.plugins.ContextMenuAPI.enabled) return removeListener(listener);
|
||||||
|
|
||||||
|
if (typeof exports !== "object" || exports === null) return;
|
||||||
|
|
||||||
|
for (const key in exports) if (key.length <= 3) {
|
||||||
|
const prop = exports[key];
|
||||||
|
if (typeof prop !== "function") continue;
|
||||||
|
|
||||||
|
const str = Function.prototype.toString.call(prop);
|
||||||
|
if (str.includes('path:["empty"]')) {
|
||||||
|
Vencord.Plugins.patches.push({
|
||||||
|
plugin: "ContextMenuAPI",
|
||||||
|
all: true,
|
||||||
|
noWarn: true,
|
||||||
|
find: "navId:",
|
||||||
|
replacement: [{
|
||||||
|
match: RegExp(`${id}(?<=(\\i)=.+?).+$`),
|
||||||
|
replace: (code, varName) => {
|
||||||
|
const regex = RegExp(`${key},{(?<=${varName}\\.${key},{)`, "g");
|
||||||
|
return code.replace(regex, "$&contextMenuApiArguments:arguments,");
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
removeListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener);
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ContextMenuAPI",
|
||||||
|
description: "API for adding/removing items to/from context menus.",
|
||||||
|
authors: [Devs.Nuckyz],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "♫ (つ。◕‿‿◕。)つ ♪",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=function \i\((\i)\){)(?=var \i,\i=\i\.navId)/,
|
||||||
|
replace: (_, props) => `Vencord.Api.ContextMenu._patchContextMenu(${props});`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
@ -43,7 +43,7 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: '"Menu API',
|
find: '"Menu API',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /function.{0,80}type===(.{1,3})\..{1,3}\).{0,50}navigable:.+?Menu API/s,
|
match: /function.{0,80}type===(\i)\).{0,50}navigable:.+?Menu API/s,
|
||||||
replace: (m, mod) => {
|
replace: (m, mod) => {
|
||||||
let nicenNames = "";
|
let nicenNames = "";
|
||||||
const redefines = [] as string[];
|
const redefines = [] as string[];
|
||||||
|
@ -29,13 +29,12 @@ export default definePlugin({
|
|||||||
find: 'displayName="NoticeStore"',
|
find: 'displayName="NoticeStore"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /;.{1,2}=null;.{0,70}getPremiumSubscription/g,
|
match: /(?=;\i=null;.{0,70}getPremiumSubscription)/g,
|
||||||
replace:
|
replace: ";if(Vencord.Api.Notices.currentNotice)return false"
|
||||||
";if(Vencord.Api.Notices.currentNotice)return false$&"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
|
match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
|
||||||
replace: 'if($1?.id=="VencordNotice")return($1=null,Vencord.Api.Notices.nextNotice(),true);'
|
replace: (_, notice) => `if(${notice}.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,6 @@ export default definePlugin({
|
|||||||
name: "WebRichPresence (arRPC)",
|
name: "WebRichPresence (arRPC)",
|
||||||
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
|
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
|
||||||
authors: [Devs.Ducko],
|
authors: [Devs.Ducko],
|
||||||
target: "WEB",
|
|
||||||
|
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => (
|
||||||
<>
|
<>
|
||||||
@ -60,6 +59,9 @@ export default definePlugin({
|
|||||||
),
|
),
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
|
// ArmCord comes with its own arRPC implementation, so this plugin just confuses users
|
||||||
|
if ("armcord" in window) return;
|
||||||
|
|
||||||
if (ws) ws.close();
|
if (ws) ws.close();
|
||||||
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
|
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
|
||||||
|
|
@ -18,6 +18,9 @@
|
|||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
import * as Webpack from "@webpack";
|
||||||
|
import { extract, filters, findAll, search } from "@webpack";
|
||||||
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
const WEB_ONLY = (f: string) => () => {
|
const WEB_ONLY = (f: string) => () => {
|
||||||
throw new Error(`'${f}' is Discord Desktop only.`);
|
throw new Error(`'${f}' is Discord Desktop only.`);
|
||||||
@ -29,19 +32,48 @@ export default definePlugin({
|
|||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
getShortcuts() {
|
getShortcuts() {
|
||||||
|
function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) {
|
||||||
|
const cache = new Map<string, unknown>();
|
||||||
|
|
||||||
|
return function (...filterProps: unknown[]) {
|
||||||
|
const cacheKey = String(filterProps);
|
||||||
|
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
||||||
|
|
||||||
|
const matches = findAll(filterFactory(...filterProps));
|
||||||
|
|
||||||
|
const result = (() => {
|
||||||
|
switch (matches.length) {
|
||||||
|
case 0: return null;
|
||||||
|
case 1: return matches[0];
|
||||||
|
default:
|
||||||
|
const uniqueMatches = [...new Set(matches)];
|
||||||
|
if (uniqueMatches.length > 1)
|
||||||
|
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches);
|
||||||
|
|
||||||
|
return matches[0];
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
if (result && cacheKey) cache.set(cacheKey, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toClip: IS_WEB ? WEB_ONLY("toClip") : window.DiscordNative.clipboard.copy,
|
|
||||||
fromClip: IS_WEB ? WEB_ONLY("fromClip") : window.DiscordNative.clipboard.read,
|
|
||||||
wp: Vencord.Webpack,
|
wp: Vencord.Webpack,
|
||||||
wpc: Vencord.Webpack.wreq.c,
|
wpc: Webpack.wreq.c,
|
||||||
wreq: Vencord.Webpack.wreq,
|
wreq: Webpack.wreq,
|
||||||
wpsearch: Vencord.Webpack.search,
|
wpsearch: search,
|
||||||
wpex: Vencord.Webpack.extract,
|
wpex: extract,
|
||||||
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
|
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
|
||||||
findByProps: Vencord.Webpack.findByProps,
|
find: newFindWrapper(f => f),
|
||||||
find: Vencord.Webpack.find,
|
findAll,
|
||||||
Plugins: Vencord.Plugins,
|
findByProps: newFindWrapper(filters.byProps),
|
||||||
React: Vencord.Webpack.Common.React,
|
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
|
||||||
|
findByCode: newFindWrapper(filters.byCode),
|
||||||
|
findAllByCode: (code: string) => findAll(filters.byCode(code)),
|
||||||
|
PluginsApi: Vencord.Plugins,
|
||||||
|
plugins: Vencord.Plugins.plugins,
|
||||||
|
React,
|
||||||
Settings: Vencord.Settings,
|
Settings: Vencord.Settings,
|
||||||
Api: Vencord.Api,
|
Api: Vencord.Api,
|
||||||
reload: () => location.reload(),
|
reload: () => location.reload(),
|
||||||
|
151
src/plugins/crashHandler.ts
Normal file
151
src/plugins/crashHandler.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { showNotification } from "@api/Notifications";
|
||||||
|
import { definePluginSettings } from "@api/settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import Logger from "@utils/Logger";
|
||||||
|
import { closeAllModals } from "@utils/modal";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { maybePromptToUpdate } from "@utils/updater";
|
||||||
|
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
|
||||||
|
const CrashHandlerLogger = new Logger("CrashHandler");
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
attemptToPreventCrashes: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Whether to attempt to prevent Discord crashes.",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
attemptToNavigateToHome: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Whether to attempt to navigate to the home when preventing Discord crashes.",
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let crashCount: number = 0;
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "CrashHandler",
|
||||||
|
description: "Utility plugin for handling and possibly recovering from Crashes without a restart",
|
||||||
|
authors: [Devs.Nuckyz],
|
||||||
|
enabledByDefault: true,
|
||||||
|
|
||||||
|
popAllModals: undefined as (() => void) | undefined,
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".Messages.ERRORS_UNEXPECTED_CRASH",
|
||||||
|
replacement: {
|
||||||
|
match: /(?=this\.setState\()/,
|
||||||
|
replace: "$self.handleCrash(this)||"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: 'dispatch({type:"MODAL_POP_ALL"})',
|
||||||
|
replacement: {
|
||||||
|
match: /"MODAL_POP_ALL".+?};(?<=(\i)=function.+?)/,
|
||||||
|
replace: (m, popAll) => `${m}$self.popAllModals=${popAll};`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
handleCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
||||||
|
if (++crashCount > 5) {
|
||||||
|
try {
|
||||||
|
showNotification({
|
||||||
|
color: "#eed202",
|
||||||
|
title: "Discord has crashed!",
|
||||||
|
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
|
||||||
|
});
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => crashCount--, 60_000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (crashCount === 1) maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true);
|
||||||
|
|
||||||
|
if (settings.store.attemptToPreventCrashes) {
|
||||||
|
this.handlePreventCrash(_this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
CrashHandlerLogger.error("Failed to handle crash", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
||||||
|
try {
|
||||||
|
showNotification({
|
||||||
|
color: "#eed202",
|
||||||
|
title: "Discord has crashed!",
|
||||||
|
body: "Attempting to recover...",
|
||||||
|
});
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
try {
|
||||||
|
FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" });
|
||||||
|
} catch (err) {
|
||||||
|
CrashHandlerLogger.debug("Failed to close open context menu.", err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.popAllModals?.();
|
||||||
|
} catch (err) {
|
||||||
|
CrashHandlerLogger.debug("Failed to close old modals.", err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
closeAllModals();
|
||||||
|
} catch (err) {
|
||||||
|
CrashHandlerLogger.debug("Failed to close all open modals.", err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
FluxDispatcher.dispatch({ type: "USER_PROFILE_MODAL_CLOSE" });
|
||||||
|
} catch (err) {
|
||||||
|
CrashHandlerLogger.debug("Failed to close user popout.", err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
|
||||||
|
} catch (err) {
|
||||||
|
CrashHandlerLogger.debug("Failed to pop all layers.", err);
|
||||||
|
}
|
||||||
|
if (settings.store.attemptToNavigateToHome) {
|
||||||
|
try {
|
||||||
|
NavigationRouter.transitionTo("/channels/@me");
|
||||||
|
} catch (err) {
|
||||||
|
CrashHandlerLogger.debug("Failed to navigate to home", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_this.forceUpdate();
|
||||||
|
} catch (err) {
|
||||||
|
CrashHandlerLogger.debug("Failed to update crash handler component.", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -19,6 +19,7 @@
|
|||||||
import { definePluginSettings } from "@api/settings";
|
import { definePluginSettings } from "@api/settings";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
import { isTruthy } from "@utils/guards";
|
||||||
import { useAwaiter } from "@utils/misc";
|
import { useAwaiter } from "@utils/misc";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
||||||
@ -56,11 +57,11 @@ interface ActivityAssets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Activity {
|
interface Activity {
|
||||||
state: string;
|
state?: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
timestamps?: {
|
timestamps?: {
|
||||||
start?: Number;
|
start?: number;
|
||||||
end?: Number;
|
end?: number;
|
||||||
};
|
};
|
||||||
assets?: ActivityAssets;
|
assets?: ActivityAssets;
|
||||||
buttons?: Array<string>;
|
buttons?: Array<string>;
|
||||||
@ -70,7 +71,7 @@ interface Activity {
|
|||||||
button_urls?: Array<string>;
|
button_urls?: Array<string>;
|
||||||
};
|
};
|
||||||
type: ActivityType;
|
type: ActivityType;
|
||||||
flags: Number;
|
flags: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ActivityType {
|
enum ActivityType {
|
||||||
@ -93,13 +94,13 @@ const numOpt = (description: string) => ({
|
|||||||
onChange: setRpc
|
onChange: setRpc
|
||||||
}) as const;
|
}) as const;
|
||||||
|
|
||||||
const choice = (label: string, value: any, _default?: Boolean) => ({
|
const choice = (label: string, value: any, _default?: boolean) => ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
default: _default
|
default: _default
|
||||||
}) as const;
|
}) as const;
|
||||||
|
|
||||||
const choiceOpt = (description: string, options) => ({
|
const choiceOpt = <T,>(description: string, options: T) => ({
|
||||||
type: OptionType.SELECT,
|
type: OptionType.SELECT,
|
||||||
description,
|
description,
|
||||||
onChange: setRpc,
|
onChange: setRpc,
|
||||||
@ -173,13 +174,13 @@ async function createActivity(): Promise<Activity | undefined> {
|
|||||||
activity.buttons = [
|
activity.buttons = [
|
||||||
buttonOneText,
|
buttonOneText,
|
||||||
buttonTwoText
|
buttonTwoText
|
||||||
].filter(Boolean);
|
].filter(isTruthy);
|
||||||
|
|
||||||
activity.metadata = {
|
activity.metadata = {
|
||||||
button_urls: [
|
button_urls: [
|
||||||
buttonOneURL,
|
buttonOneURL,
|
||||||
buttonTwoURL
|
buttonTwoURL
|
||||||
].filter(Boolean)
|
].filter(isTruthy)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,12 +207,10 @@ async function createActivity(): Promise<Activity | undefined> {
|
|||||||
delete activity[k];
|
delete activity[k];
|
||||||
}
|
}
|
||||||
|
|
||||||
// WHAT DO YOU WANT FROM ME
|
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setRpc(disable?: Boolean) {
|
async function setRpc(disable?: boolean) {
|
||||||
const activity: Activity | undefined = await createActivity();
|
const activity: Activity | undefined = await createActivity();
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
|
252
src/plugins/devCompanion.dev.tsx
Normal file
252
src/plugins/devCompanion.dev.tsx
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
/*
|
||||||
|
* 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 { addContextMenuPatch } from "@api/ContextMenu";
|
||||||
|
import { showNotification } from "@api/Notifications";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import Logger from "@utils/Logger";
|
||||||
|
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { filters, findAll, search } from "@webpack";
|
||||||
|
import { Menu } from "@webpack/common";
|
||||||
|
|
||||||
|
const PORT = 8485;
|
||||||
|
const NAV_ID = "dev-companion-reconnect";
|
||||||
|
|
||||||
|
const logger = new Logger("DevCompanion");
|
||||||
|
|
||||||
|
let socket: WebSocket | undefined;
|
||||||
|
|
||||||
|
type Node = StringNode | RegexNode | FunctionNode;
|
||||||
|
|
||||||
|
interface StringNode {
|
||||||
|
type: "string";
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RegexNode {
|
||||||
|
type: "regex";
|
||||||
|
value: {
|
||||||
|
pattern: string;
|
||||||
|
flags: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FunctionNode {
|
||||||
|
type: "function";
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PatchData {
|
||||||
|
find: string;
|
||||||
|
replacement: {
|
||||||
|
match: StringNode | RegexNode;
|
||||||
|
replace: StringNode | FunctionNode;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FindData {
|
||||||
|
type: string;
|
||||||
|
args: Array<StringNode | FunctionNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNode(node: Node) {
|
||||||
|
switch (node.type) {
|
||||||
|
case "string":
|
||||||
|
return node.value;
|
||||||
|
case "regex":
|
||||||
|
return new RegExp(node.value.pattern, node.value.flags);
|
||||||
|
case "function":
|
||||||
|
// We LOVE remote code execution
|
||||||
|
// Safety: This comes from localhost only, which actually means we have less permissions than the source,
|
||||||
|
// since we're running in the browser sandbox, whereas the sender has host access
|
||||||
|
return (0, eval)(node.value);
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown Node Type " + (node as any).type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initWs(isManual = false) {
|
||||||
|
let wasConnected = isManual;
|
||||||
|
let hasErrored = false;
|
||||||
|
const ws = socket = new WebSocket(`ws://localhost:${PORT}`);
|
||||||
|
|
||||||
|
ws.addEventListener("open", () => {
|
||||||
|
wasConnected = true;
|
||||||
|
|
||||||
|
logger.info("Connected to WebSocket");
|
||||||
|
|
||||||
|
showNotification({
|
||||||
|
title: "Dev Companion Connected",
|
||||||
|
body: "Connected to WebSocket"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("error", e => {
|
||||||
|
if (!wasConnected) return;
|
||||||
|
|
||||||
|
hasErrored = true;
|
||||||
|
|
||||||
|
logger.error("Dev Companion Error:", e);
|
||||||
|
|
||||||
|
showNotification({
|
||||||
|
title: "Dev Companion Error",
|
||||||
|
body: (e as ErrorEvent).message || "No Error Message",
|
||||||
|
color: "var(--status-danger, red)"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("close", e => {
|
||||||
|
if (!wasConnected || hasErrored) return;
|
||||||
|
|
||||||
|
logger.info("Dev Companion Disconnected:", e.code, e.reason);
|
||||||
|
|
||||||
|
showNotification({
|
||||||
|
title: "Dev Companion Disconnected",
|
||||||
|
body: e.reason || "No Reason provided",
|
||||||
|
color: "var(--status-danger, red)"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("message", e => {
|
||||||
|
try {
|
||||||
|
var { nonce, type, data } = JSON.parse(e.data);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Invalid JSON:", err, "\n" + e.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reply(error?: string) {
|
||||||
|
const data = { nonce, ok: !error } as Record<string, unknown>;
|
||||||
|
if (error) data.error = error;
|
||||||
|
|
||||||
|
ws.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Received Message:", type, "\n", data);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "testPatch": {
|
||||||
|
const { find, replacement } = data as PatchData;
|
||||||
|
|
||||||
|
const candidates = search(find);
|
||||||
|
const keys = Object.keys(candidates);
|
||||||
|
if (keys.length !== 1)
|
||||||
|
return reply("Expected exactly one 'find' matches, found " + keys.length);
|
||||||
|
|
||||||
|
let src = String(candidates[keys[0]]);
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
for (const { match, replace } of replacement) {
|
||||||
|
i++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const matcher = canonicalizeMatch(parseNode(match));
|
||||||
|
const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName");
|
||||||
|
|
||||||
|
const newSource = src.replace(matcher, replacement as string);
|
||||||
|
|
||||||
|
if (src === newSource) throw "Had no effect";
|
||||||
|
Function(newSource);
|
||||||
|
|
||||||
|
src = newSource;
|
||||||
|
} catch (err) {
|
||||||
|
return reply(`Replacement ${i} failed: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reply();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "testFind": {
|
||||||
|
const { type, args } = data as FindData;
|
||||||
|
try {
|
||||||
|
var parsedArgs = args.map(parseNode);
|
||||||
|
} catch (err) {
|
||||||
|
return reply("Failed to parse args: " + err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let results: any[];
|
||||||
|
switch (type.replace("find", "").replace("Lazy", "")) {
|
||||||
|
case "":
|
||||||
|
results = findAll(parsedArgs[0]);
|
||||||
|
break;
|
||||||
|
case "ByProps":
|
||||||
|
results = findAll(filters.byProps(...parsedArgs));
|
||||||
|
break;
|
||||||
|
case "Store":
|
||||||
|
results = findAll(filters.byStoreName(parsedArgs[0]));
|
||||||
|
break;
|
||||||
|
case "ByCode":
|
||||||
|
results = findAll(filters.byCode(...parsedArgs));
|
||||||
|
break;
|
||||||
|
case "ModuleId":
|
||||||
|
results = Object.keys(search(parsedArgs[0]));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return reply("Unknown Find Type " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueResultsCount = new Set(results).size;
|
||||||
|
if (uniqueResultsCount === 0) throw "No results";
|
||||||
|
if (uniqueResultsCount > 1) throw "Found more than one result! Make this filter more specific";
|
||||||
|
} catch (err) {
|
||||||
|
return reply("Failed to find: " + err);
|
||||||
|
}
|
||||||
|
|
||||||
|
reply();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
reply("Unknown Type " + type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "DevCompanion",
|
||||||
|
description: "Dev Companion Plugin",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
dependencies: ["ContextMenuAPI"],
|
||||||
|
|
||||||
|
start() {
|
||||||
|
initWs();
|
||||||
|
addContextMenuPatch("user-settings-cog", kids => {
|
||||||
|
if (kids.some(k => k?.props?.id === NAV_ID)) return;
|
||||||
|
|
||||||
|
kids.unshift(
|
||||||
|
<Menu.MenuItem
|
||||||
|
id={NAV_ID}
|
||||||
|
label="Reconnect Dev Companion"
|
||||||
|
action={() => {
|
||||||
|
socket?.close(1000, "Reconnecting");
|
||||||
|
initWs(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
socket?.close(1000, "Plugin Stopped");
|
||||||
|
socket = void 0;
|
||||||
|
}
|
||||||
|
});
|
@ -27,9 +27,9 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
|
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /function (?<functionName>.{1,3})\(\){.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT.+?}}/,
|
match: /(?<=function \i\(\){)(?=.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT)/,
|
||||||
replace: "function $<functionName>(){}",
|
replace: "return;"
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
|
@ -16,15 +16,16 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { migratePluginSettings, Settings } from "@api/settings";
|
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||||
|
import { migratePluginSettings } from "@api/settings";
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { makeLazy } from "@utils/misc";
|
import { Margins } from "@utils/margins";
|
||||||
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||||
import { Forms, GuildStore, Margins, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
|
import { Forms, GuildStore, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
|
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
|
||||||
|
|
||||||
@ -96,7 +97,7 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.marginTop20}>Custom Name</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20}>Custom Name</Forms.FormTitle>
|
||||||
<CheckedTextInput
|
<CheckedTextInput
|
||||||
value={name}
|
value={name}
|
||||||
onChange={setName}
|
onChange={setName}
|
||||||
@ -175,72 +176,74 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, args) => {
|
||||||
|
if (!args?.[0]) return;
|
||||||
|
const { favoriteableId, emoteClonerDataAlt, itemHref, itemSrc, favoriteableType } = args[0];
|
||||||
|
|
||||||
|
if (!emoteClonerDataAlt || favoriteableType !== "emoji") return;
|
||||||
|
|
||||||
|
const name = emoteClonerDataAlt.match(/:(.*)(?:~\d+)?:/)?.[1];
|
||||||
|
if (!name || !favoriteableId) return;
|
||||||
|
|
||||||
|
const src = itemHref ?? itemSrc;
|
||||||
|
const isAnimated = new URL(src).pathname.endsWith(".gif");
|
||||||
|
|
||||||
|
const group = findGroupChildrenByChildId("save-image", children);
|
||||||
|
if (group && !group.some(child => child?.props?.id === "emote-cloner")) {
|
||||||
|
group.push((
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="emote-cloner"
|
||||||
|
key="emote-cloner"
|
||||||
|
label="Clone"
|
||||||
|
action={() =>
|
||||||
|
openModal(modalProps => (
|
||||||
|
<ModalRoot {...modalProps}>
|
||||||
|
<ModalHeader>
|
||||||
|
<img
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden
|
||||||
|
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${favoriteableId}.${isAnimated ? "gif" : "png"}`}
|
||||||
|
alt=""
|
||||||
|
height={24}
|
||||||
|
width={24}
|
||||||
|
style={{ marginRight: "0.5em" }}
|
||||||
|
/>
|
||||||
|
<Forms.FormText>Clone {name}</Forms.FormText>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalContent>
|
||||||
|
<CloneModal id={favoriteableId} name={name} isAnimated={isAnimated} />
|
||||||
|
</ModalContent>
|
||||||
|
</ModalRoot>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
</Menu.MenuItem>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
migratePluginSettings("EmoteCloner", "EmoteYoink");
|
migratePluginSettings("EmoteCloner", "EmoteYoink");
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "EmoteCloner",
|
name: "EmoteCloner",
|
||||||
description: "Adds a Clone context menu item to emotes to clone them your own server",
|
description: "Adds a Clone context menu item to emotes to clone them your own server",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven, Devs.Nuckyz],
|
||||||
dependencies: ["MenuItemDeobfuscatorAPI"],
|
dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
|
||||||
|
|
||||||
patches: [{
|
patches: [
|
||||||
// Literally copy pasted from ReverseImageSearch lol
|
{
|
||||||
find: "open-native-link",
|
find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
|
match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/,
|
||||||
replace: "$&,$self.makeMenu(arguments[2])"
|
replace: (m, target) => `${m}emoteClonerDataAlt:${target}.alt,`
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
// Also copy pasted from Reverse Image Search
|
|
||||||
{
|
|
||||||
// pass the target to the open link menu so we can grab its data
|
|
||||||
find: "REMOVE_ALL_REACTIONS_CONFIRM_BODY,",
|
|
||||||
predicate: makeLazy(() => !Settings.plugins.ReverseImageSearch.enabled),
|
|
||||||
noWarn: true,
|
|
||||||
replacement: {
|
|
||||||
match: /(?<props>.).onHeightUpdate.{0,200}(.)=(.)=.\.url;.+?\(null!=\3\?\3:\2[^)]+/,
|
|
||||||
replace: "$&,$<props>.target"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
makeMenu(htmlElement: HTMLImageElement) {
|
|
||||||
if (htmlElement?.dataset.type !== "emoji")
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const { id } = htmlElement.dataset;
|
|
||||||
const name = htmlElement.alt.match(/:(.*)(?:~\d+)?:/)?.[1];
|
|
||||||
|
|
||||||
if (!name || !id)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const isAnimated = new URL(htmlElement.src).pathname.endsWith(".gif");
|
|
||||||
|
|
||||||
return <Menu.MenuItem
|
|
||||||
id="emote-cloner"
|
|
||||||
key="emote-cloner"
|
|
||||||
label="Clone"
|
|
||||||
action={() =>
|
|
||||||
openModal(modalProps => (
|
|
||||||
<ModalRoot {...modalProps}>
|
|
||||||
<ModalHeader>
|
|
||||||
<img
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden
|
|
||||||
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${isAnimated ? "gif" : "png"}`}
|
|
||||||
alt=""
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
style={{ marginRight: "0.5em" }}
|
|
||||||
/>
|
|
||||||
<Forms.FormText>Clone {name}</Forms.FormText>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalContent>
|
|
||||||
<CloneModal id={id} name={name} isAnimated={isAnimated} />
|
|
||||||
</ModalContent>
|
|
||||||
</ModalRoot>
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
>
|
}
|
||||||
</Menu.MenuItem>;
|
],
|
||||||
|
|
||||||
|
start() {
|
||||||
|
addContextMenuPatch("message", messageContextMenuPatch);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
removeContextMenuPatch("message", messageContextMenuPatch);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
import { definePluginSettings } from "@api/settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
@ -24,49 +24,71 @@ import { Forms, React } from "@webpack/common";
|
|||||||
|
|
||||||
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
|
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
enableIsStaff: {
|
||||||
|
description: "Enable isStaff",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false,
|
||||||
|
restartNeeded: true
|
||||||
|
},
|
||||||
|
forceStagingBanner: {
|
||||||
|
description: "Whether to force Staging banner under user area.",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false,
|
||||||
|
restartNeeded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Experiments",
|
name: "Experiments",
|
||||||
|
description: "Enable Access to Experiments in Discord!",
|
||||||
authors: [
|
authors: [
|
||||||
Devs.Megu,
|
Devs.Megu,
|
||||||
Devs.Ven,
|
Devs.Ven,
|
||||||
Devs.Nickyux,
|
Devs.Nickyux,
|
||||||
Devs.BanTheNons
|
Devs.BanTheNons,
|
||||||
|
Devs.Nuckyz
|
||||||
],
|
],
|
||||||
description: "Enable Access to Experiments in Discord!",
|
settings,
|
||||||
patches: [{
|
|
||||||
find: "Object.defineProperties(this,{isDeveloper",
|
patches: [
|
||||||
replacement: {
|
{
|
||||||
match: /(?<={isDeveloper:\{[^}]+,get:function\(\)\{return )\w/,
|
find: "Object.defineProperties(this,{isDeveloper",
|
||||||
replace: "true"
|
replacement: {
|
||||||
|
match: /(?<={isDeveloper:\{[^}]+?,get:function\(\)\{return )\w/,
|
||||||
|
replace: "true"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}, {
|
{
|
||||||
find: 'type:"user",revision',
|
find: 'type:"user",revision',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /!(\w{1,3})&&"CONNECTION_OPEN".+?;/g,
|
match: /!(\i)&&"CONNECTION_OPEN".+?;/g,
|
||||||
replace: "$1=!0;"
|
replace: "$1=!0;"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}, {
|
{
|
||||||
find: ".isStaff=function(){",
|
find: ".isStaff=function(){",
|
||||||
predicate: () => Settings.plugins.Experiments.enableIsStaff === true,
|
predicate: () => settings.store.enableIsStaff,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /return\s*(\w+)\.hasFlag\((.+?)\.STAFF\)}/,
|
match: /return\s*?(\i)\.hasFlag\((\i\.\i)\.STAFF\)}/,
|
||||||
replace: "return Vencord.Webpack.Common.UserStore.getCurrentUser().id===$1.id||$1.hasFlag($2.STAFF)}"
|
replace: (_, user, flags) => `return Vencord.Webpack.Common.UserStore.getCurrentUser().id===${user}.id||${user}.hasFlag(${flags}.STAFF)}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*\|\|/,
|
match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*?\|\|/,
|
||||||
replace: "hasFreePremium=function(){return ",
|
replace: "hasFreePremium=function(){return ",
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
}],
|
},
|
||||||
options: {
|
{
|
||||||
enableIsStaff: {
|
find: ".Messages.DEV_NOTICE_STAGING",
|
||||||
description: "Enable isStaff (requires restart)",
|
predicate: () => settings.store.forceStagingBanner,
|
||||||
type: OptionType.BOOLEAN,
|
replacement: {
|
||||||
default: false,
|
match: /"staging"===window\.GLOBAL_ENV\.RELEASE_CHANNEL/,
|
||||||
restartNeeded: true,
|
replace: "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
],
|
||||||
|
|
||||||
settingsAboutComponent: () => {
|
settingsAboutComponent: () => {
|
||||||
const isMacOS = navigator.platform.includes("Mac");
|
const isMacOS = navigator.platform.includes("Mac");
|
||||||
|
@ -72,7 +72,7 @@ migratePluginSettings("FakeNitro", "NitroBypass");
|
|||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FakeNitro",
|
name: "FakeNitro",
|
||||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain],
|
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz],
|
||||||
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 and use client themes.",
|
||||||
dependencies: ["MessageEventsAPI"],
|
dependencies: ["MessageEventsAPI"],
|
||||||
|
|
||||||
@ -82,16 +82,16 @@ export default definePlugin({
|
|||||||
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=(?<intention>\i)=\i\.intention)/,
|
match: /(?<=(\i)=\i\.intention)/,
|
||||||
replace: ",fakeNitroIntention=$<intention>"
|
replace: (_, intention) => `,fakeNitroIntention=${intention}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g,
|
match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g,
|
||||||
replace: ",fakeNitroIntention"
|
replace: ',typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=&&!\i&&)!(?<canUseExternal>\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
match: /(?<=&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
||||||
replace: `(!$<canUseExternal>&&![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
|
replace: (_, canUseExternal) => `(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -99,16 +99,16 @@ export default definePlugin({
|
|||||||
find: "canUseAnimatedEmojis:function",
|
find: "canUseAnimatedEmojis:function",
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\((?<user>\i))\){(?<premiumCheck>.+?\))/g,
|
match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g,
|
||||||
replace: `,fakeNitroIntention){$<premiumCheck>||fakeNitroIntention===undefined||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
replace: (_, premiumCheck) => `,fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "canUseStickersEverywhere:function",
|
find: "canUseStickersEverywhere:function",
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /canUseStickersEverywhere:function\(.+?\{/,
|
match: /(?<=canUseStickersEverywhere:function\(\i\){)/,
|
||||||
replace: "$&return true;"
|
replace: "return true;"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -128,8 +128,8 @@ export default definePlugin({
|
|||||||
"canStreamMidQuality"
|
"canStreamMidQuality"
|
||||||
].map(func => {
|
].map(func => {
|
||||||
return {
|
return {
|
||||||
match: new RegExp(`${func}:function\\(.+?\\{`),
|
match: new RegExp(`(?<=${func}:function\\(\\i\\){)`),
|
||||||
replace: "$&return true;"
|
replace: "return true;"
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
56
src/plugins/fixInbox.tsx
Normal file
56
src/plugins/fixInbox.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { Forms } from "@webpack/common";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "FixInbox",
|
||||||
|
description: "Fixes the Unreads Inbox from crashing Discord when you're in lots of guilds.",
|
||||||
|
authors: [Devs.Megu],
|
||||||
|
|
||||||
|
patches: [{
|
||||||
|
find: "INBOX_OPEN:function",
|
||||||
|
replacement: {
|
||||||
|
// This function normally dispatches a subscribe event to every guild.
|
||||||
|
// this is badbadbadbadbad so we just get rid of it.
|
||||||
|
match: /INBOX_OPEN:function.+?\{/,
|
||||||
|
replace: "$&return true;"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
|
||||||
|
settingsAboutComponent() {
|
||||||
|
return (
|
||||||
|
<Forms.FormSection>
|
||||||
|
<Forms.FormTitle tag="h3">What's the problem?</Forms.FormTitle>
|
||||||
|
<Forms.FormText style={{ marginBottom: 8 }}>
|
||||||
|
By default, Discord emits a GUILD_SUBSCRIPTIONS event for every guild you're in.
|
||||||
|
When you're in a lot of guilds, this can cause the gateway to ratelimit you.
|
||||||
|
This causes the client to crash and get stuck in an infinite ratelimit loop as it tries to reconnect.
|
||||||
|
</Forms.FormText>
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h3">How does it work?</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
This plugin works by stopping the client from sending GUILD_SUBSCRIPTIONS events to the gateway when you open the unreads inbox.
|
||||||
|
This means that not all unreads will be shown, instead only already-subscribed guilds' unreads will be shown, but your client won't crash anymore.
|
||||||
|
</Forms.FormText>
|
||||||
|
</Forms.FormSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -21,7 +21,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { useForceUpdater } from "@utils/misc";
|
import { useForceUpdater } from "@utils/misc";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
import { Tooltip } from "webpack/common";
|
import { Tooltip } from "webpack/common";
|
||||||
|
|
||||||
enum ActivitiesTypes {
|
enum ActivitiesTypes {
|
||||||
@ -37,7 +37,7 @@ interface IgnoredActivity {
|
|||||||
const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
|
const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
|
||||||
const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon");
|
const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon");
|
||||||
const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
|
const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
|
||||||
const RunningGameStore = findByPropsLazy("getRunningGames", "getGamesSeen");
|
const RunningGameStore = findStoreLazy("RunningGameStore");
|
||||||
|
|
||||||
function ToggleIconOff() {
|
function ToggleIconOff() {
|
||||||
return (
|
return (
|
||||||
@ -71,7 +71,7 @@ function ToggleIconOff() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToggleIconOn() {
|
function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className={RegisteredGamesClasses.overlayToggleIconOn}
|
className={RegisteredGamesClasses.overlayToggleIconOn}
|
||||||
@ -80,14 +80,15 @@ function ToggleIconOn() {
|
|||||||
viewBox="0 0 32 26"
|
viewBox="0 0 32 26"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
className={RegisteredGamesClasses.fill}
|
className={forceWhite ? "" : RegisteredGamesClasses.fill}
|
||||||
|
fill={forceWhite ? "var(--white-500)" : ""}
|
||||||
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
|
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToggleActivityComponent({ activity }: { activity: IgnoredActivity; }) {
|
function ToggleActivityComponent({ activity, forceWhite }: { activity: IgnoredActivity; forceWhite?: boolean; }) {
|
||||||
const forceUpdate = useForceUpdater();
|
const forceUpdate = useForceUpdater();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -105,7 +106,7 @@ function ToggleActivityComponent({ activity }: { activity: IgnoredActivity; }) {
|
|||||||
{
|
{
|
||||||
ignoredActivitiesCache.has(activity.id)
|
ignoredActivitiesCache.has(activity.id)
|
||||||
? <ToggleIconOff />
|
? <ToggleIconOff />
|
||||||
: <ToggleIconOn />
|
: <ToggleIconOn forceWhite={forceWhite} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -117,9 +118,9 @@ function ToggleActivityComponentWithBackground({ activity }: { activity: Ignored
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
|
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
|
||||||
style={{ padding: "0 2px" }}
|
style={{ padding: "0px 2px" }}
|
||||||
>
|
>
|
||||||
<ToggleActivityComponent activity={activity} />
|
<ToggleActivityComponent activity={activity} forceWhite={true} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -142,25 +143,32 @@ export default definePlugin({
|
|||||||
name: "IgnoreActivities",
|
name: "IgnoreActivities",
|
||||||
authors: [Devs.Nuckyz],
|
authors: [Devs.Nuckyz],
|
||||||
description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.",
|
description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.",
|
||||||
patches: [{
|
patches: [
|
||||||
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
|
{
|
||||||
replacement: {
|
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
|
||||||
match: /var .=(?<props>.)\.overlay.+?"aria-label":.\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY.+?}}\)/,
|
replacement: {
|
||||||
replace: "$&,$self.renderToggleGameActivityButton($<props>)"
|
match: /!(\i)\|\|(null==\i\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/,
|
||||||
|
replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => ""
|
||||||
|
+ `${restWithoutPlatformCheck}`
|
||||||
|
+ `(${platformCheck}?${children}:[])`
|
||||||
|
+ `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".overlayBadge",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=\(\)\.badgeContainer.+?(\i)\.name}\):null)/,
|
||||||
|
replace: (_, props) => `,$self.renderToggleActivityButton(${props})`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: '.displayName="LocalActivityStore"',
|
||||||
|
replacement: {
|
||||||
|
match: /LISTENING.+?\)\);(?<=(\i)\.push.+?)/,
|
||||||
|
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, {
|
],
|
||||||
find: ".overlayBadge",
|
|
||||||
replacement: {
|
|
||||||
match: /.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?<props>.)\.name}\):null/,
|
|
||||||
replace: "$&,$self.renderToggleActivityButton($<props>)"
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
find: '.displayName="LocalActivityStore"',
|
|
||||||
replacement: {
|
|
||||||
match: /(?<activities>.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?\)\)/,
|
|
||||||
replace: "$&;$<activities>=$<activities>.filter($self.isActivityNotIgnored);"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
const ignoredActivitiesData = await DataStore.get<string[] | Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities") ?? new Map<IgnoredActivity["id"], IgnoredActivity>();
|
const ignoredActivitiesData = await DataStore.get<string[] | Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities") ?? new Map<IgnoredActivity["id"], IgnoredActivity>();
|
||||||
@ -214,5 +222,5 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
@ -91,8 +91,8 @@ function ChatBarIcon() {
|
|||||||
<svg
|
<svg
|
||||||
aria-hidden
|
aria-hidden
|
||||||
role="img"
|
role="img"
|
||||||
width="24"
|
width="32"
|
||||||
height="24"
|
height="32"
|
||||||
viewBox={"0 0 64 64"}
|
viewBox={"0 0 64 64"}
|
||||||
style={{ scale: "1.1" }}
|
style={{ scale: "1.1" }}
|
||||||
>
|
>
|
||||||
@ -131,8 +131,8 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: ".activeCommandOption",
|
find: ".activeCommandOption",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /.=.\.activeCommand,.=.\.activeCommandOption,.{1,133}(.)=\[\];/,
|
match: /(.)\.push.{1,50}\(\i,\{.{1,30}\},"gift"\)\)/,
|
||||||
replace: "$&;$1.push($self.chatBarIcon());",
|
replace: "$&;try{$1.push($self.chatBarIcon())}catch{}",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -34,7 +34,7 @@ interface Activity {
|
|||||||
state: string;
|
state: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
timestamps?: {
|
timestamps?: {
|
||||||
start?: Number;
|
start?: number;
|
||||||
};
|
};
|
||||||
assets?: ActivityAssets;
|
assets?: ActivityAssets;
|
||||||
buttons?: Array<string>;
|
buttons?: Array<string>;
|
||||||
@ -43,8 +43,8 @@ interface Activity {
|
|||||||
metadata?: {
|
metadata?: {
|
||||||
button_urls?: Array<string>;
|
button_urls?: Array<string>;
|
||||||
};
|
};
|
||||||
type: Number;
|
type: number;
|
||||||
flags: Number;
|
flags: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TrackData {
|
interface TrackData {
|
||||||
|
@ -17,11 +17,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { addAccessory } from "@api/MessageAccessories";
|
import { addAccessory } from "@api/MessageAccessories";
|
||||||
import { Settings } from "@api/settings";
|
import { definePluginSettings } from "@api/settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants.js";
|
import { Devs } from "@utils/constants.js";
|
||||||
|
import { classes, LazyComponent } from "@utils/misc";
|
||||||
import { Queue } from "@utils/Queue";
|
import { Queue } from "@utils/Queue";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { filters, findByPropsLazy, waitFor } from "@webpack";
|
import { find, findByCode, findByPropsLazy } from "@webpack";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ChannelStore,
|
ChannelStore,
|
||||||
@ -36,41 +38,20 @@ import {
|
|||||||
} from "@webpack/common";
|
} from "@webpack/common";
|
||||||
import { Channel, Guild, Message } from "discord-types/general";
|
import { Channel, Guild, Message } from "discord-types/general";
|
||||||
|
|
||||||
let messageCache: { [id: string]: { message?: Message, fetched: boolean; }; } = {};
|
const messageCache = new Map<string, {
|
||||||
|
message?: Message;
|
||||||
|
fetched: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
let AutomodEmbed: React.ComponentType<any>,
|
const Embed = LazyComponent(() => findByCode(".inlineMediaEmbed"));
|
||||||
Embed: React.ComponentType<any>,
|
const ChannelMessage = LazyComponent(() => find(m => m.type?.toString()?.includes('["message","compact","className",')));
|
||||||
ChannelMessage: React.ComponentType<any>,
|
|
||||||
Endpoints: Record<string, any>;
|
|
||||||
|
|
||||||
waitFor(["mle_AutomodEmbed"], m => (AutomodEmbed = m.mle_AutomodEmbed));
|
|
||||||
waitFor(filters.byCode(".inlineMediaEmbed"), m => Embed = m);
|
|
||||||
waitFor(m => m.type?.toString()?.includes('["message","compact","className",'), m => ChannelMessage = m);
|
|
||||||
waitFor(["MESSAGE_CREATE_ATTACHMENT_UPLOAD"], _ => Endpoints = _);
|
|
||||||
const SearchResultClasses = findByPropsLazy("message", "searchResult");
|
const SearchResultClasses = findByPropsLazy("message", "searchResult");
|
||||||
|
|
||||||
const messageFetchQueue = new Queue();
|
let AutoModEmbed: React.ComponentType<any> = () => null;
|
||||||
async function fetchMessage(channelID: string, messageID: string): Promise<Message | void> {
|
|
||||||
if (messageID in messageCache && !messageCache[messageID].fetched) return Promise.resolve();
|
|
||||||
if (messageCache[messageID]?.fetched) return Promise.resolve(messageCache[messageID].message);
|
|
||||||
|
|
||||||
messageCache[messageID] = { fetched: false };
|
const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
|
||||||
const res = await RestAPI.get({
|
const tenorRegex = /https:\/\/(?:www.)?tenor\.com/;
|
||||||
url: Endpoints.MESSAGES(channelID),
|
|
||||||
query: {
|
|
||||||
limit: 1,
|
|
||||||
around: messageID
|
|
||||||
},
|
|
||||||
retries: 2
|
|
||||||
}).catch(() => { });
|
|
||||||
const apiMessage = res.body?.[0];
|
|
||||||
const message: Message = MessageStore.getMessages(apiMessage.channel_id).receiveMessage(apiMessage).get(apiMessage.id);
|
|
||||||
messageCache[message.id] = {
|
|
||||||
message: message,
|
|
||||||
fetched: true
|
|
||||||
};
|
|
||||||
return Promise.resolve(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Attachment {
|
interface Attachment {
|
||||||
height: number;
|
height: number;
|
||||||
@ -79,66 +60,133 @@ interface Attachment {
|
|||||||
proxyURL?: string;
|
proxyURL?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTenorGif = /https:\/\/(?:www.)?tenor\.com/;
|
|
||||||
function getImages(message: Message): Attachment[] {
|
|
||||||
const attachments: Attachment[] = [];
|
|
||||||
message.attachments?.forEach(a => {
|
|
||||||
if (a.content_type!.startsWith("image/")) attachments.push({
|
|
||||||
height: a.height!,
|
|
||||||
width: a.width!,
|
|
||||||
url: a.url,
|
|
||||||
proxyURL: a.proxy_url!
|
|
||||||
});
|
|
||||||
});
|
|
||||||
message.embeds?.forEach(e => {
|
|
||||||
if (e.type === "image") attachments.push(
|
|
||||||
e.image ? { ...e.image } : { ...e.thumbnail! }
|
|
||||||
);
|
|
||||||
if (e.type === "gifv" && !isTenorGif.test(e.url!)) {
|
|
||||||
attachments.push({
|
|
||||||
height: e.thumbnail!.height,
|
|
||||||
width: e.thumbnail!.width,
|
|
||||||
url: e.url!
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return attachments;
|
|
||||||
}
|
|
||||||
|
|
||||||
const noContent = (attachments: number, embeds: number): string => {
|
|
||||||
if (!attachments && !embeds) return "";
|
|
||||||
if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
|
||||||
if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""}]`;
|
|
||||||
return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""} and ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
|
||||||
};
|
|
||||||
|
|
||||||
function requiresRichEmbed(message: Message) {
|
|
||||||
if (message.attachments.every(a => a.content_type?.startsWith("image/"))
|
|
||||||
&& message.embeds.every(e => e.type === "image" || (e.type === "gifv" && !isTenorGif.test(e.url!)))
|
|
||||||
&& !message.components.length
|
|
||||||
) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const computeWidthAndHeight = (width: number, height: number) => {
|
|
||||||
const maxWidth = 400, maxHeight = 300;
|
|
||||||
let newWidth: number, newHeight: number;
|
|
||||||
if (width > height) {
|
|
||||||
newWidth = Math.min(width, maxWidth);
|
|
||||||
newHeight = Math.round(height / (width / newWidth));
|
|
||||||
} else {
|
|
||||||
newHeight = Math.min(height, maxHeight);
|
|
||||||
newWidth = Math.round(width / (height / newHeight));
|
|
||||||
}
|
|
||||||
return { width: newWidth, height: newHeight };
|
|
||||||
};
|
|
||||||
|
|
||||||
interface MessageEmbedProps {
|
interface MessageEmbedProps {
|
||||||
message: Message;
|
message: Message;
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
guildID: string;
|
guildID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messageFetchQueue = new Queue();
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
messageBackgroundColor: {
|
||||||
|
description: "Background color for messages in rich embeds",
|
||||||
|
type: OptionType.BOOLEAN
|
||||||
|
},
|
||||||
|
automodEmbeds: {
|
||||||
|
description: "Use automod embeds instead of rich embeds (smaller but less info)",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Always use automod embeds",
|
||||||
|
value: "always"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Prefer automod embeds, but use rich embeds if some content can't be shown",
|
||||||
|
value: "prefer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Never use automod embeds",
|
||||||
|
value: "never",
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
clearMessageCache: {
|
||||||
|
type: OptionType.COMPONENT,
|
||||||
|
description: "Clear the linked message cache",
|
||||||
|
component: () =>
|
||||||
|
<Button onClick={() => messageCache.clear()}>
|
||||||
|
Clear the linked message cache
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
async function fetchMessage(channelID: string, messageID: string) {
|
||||||
|
const cached = messageCache.get(messageID);
|
||||||
|
if (cached) return cached.message;
|
||||||
|
|
||||||
|
messageCache.set(messageID, { fetched: false });
|
||||||
|
|
||||||
|
const res = await RestAPI.get({
|
||||||
|
url: `/channels/${channelID}/messages`,
|
||||||
|
query: {
|
||||||
|
limit: 1,
|
||||||
|
around: messageID
|
||||||
|
},
|
||||||
|
retries: 2
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
const msg = res?.body?.[0];
|
||||||
|
if (!msg) return;
|
||||||
|
|
||||||
|
const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id);
|
||||||
|
|
||||||
|
messageCache.set(message.id, {
|
||||||
|
message,
|
||||||
|
fetched: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getImages(message: Message): Attachment[] {
|
||||||
|
const attachments: Attachment[] = [];
|
||||||
|
|
||||||
|
for (const { content_type, height, width, url, proxy_url } of message.attachments ?? []) {
|
||||||
|
if (content_type?.startsWith("image/"))
|
||||||
|
attachments.push({
|
||||||
|
height: height!,
|
||||||
|
width: width!,
|
||||||
|
url: url,
|
||||||
|
proxyURL: proxy_url!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { type, image, thumbnail, url } of message.embeds ?? []) {
|
||||||
|
if (type === "image")
|
||||||
|
attachments.push({ ...(image ?? thumbnail!) });
|
||||||
|
else if (url && type === "gifv" && !tenorRegex.test(url))
|
||||||
|
attachments.push({
|
||||||
|
height: thumbnail!.height,
|
||||||
|
width: thumbnail!.width,
|
||||||
|
url
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function noContent(attachments: number, embeds: number) {
|
||||||
|
if (!attachments && !embeds) return "";
|
||||||
|
if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
||||||
|
if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""}]`;
|
||||||
|
return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""} and ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiresRichEmbed(message: Message) {
|
||||||
|
if (message.components.length) return true;
|
||||||
|
if (message.attachments.some(a => !a.content_type?.startsWith("image/"))) return true;
|
||||||
|
if (message.embeds.some(e => e.type !== "image" && (e.type !== "gifv" || tenorRegex.test(e.url!)))) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeWidthAndHeight(width: number, height: number) {
|
||||||
|
const maxWidth = 400;
|
||||||
|
const maxHeight = 300;
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
const adjustedWidth = Math.min(width, maxWidth);
|
||||||
|
return { width: adjustedWidth, height: Math.round(height / (width / adjustedWidth)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustedHeight = Math.min(height, maxHeight);
|
||||||
|
return { width: Math.round(width / (height / adjustedHeight)), height: adjustedHeight };
|
||||||
|
}
|
||||||
|
|
||||||
function withEmbeddedBy(message: Message, embeddedBy: string[]) {
|
function withEmbeddedBy(message: Message, embeddedBy: string[]) {
|
||||||
return new Proxy(message, {
|
return new Proxy(message, {
|
||||||
get(_, prop) {
|
get(_, prop) {
|
||||||
@ -149,181 +197,172 @@ function withEmbeddedBy(message: Message, embeddedBy: string[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function MessageEmbedAccessory({ message }: { message: Message; }) {
|
||||||
|
// @ts-ignore
|
||||||
|
const embeddedBy: string[] = message.vencordEmbeddedBy ?? [];
|
||||||
|
|
||||||
|
const accessories = [] as (JSX.Element | null)[];
|
||||||
|
|
||||||
|
let match = null as RegExpMatchArray | null;
|
||||||
|
while ((match = messageLinkRegex.exec(message.content!)) !== null) {
|
||||||
|
const [_, guildID, channelID, messageID] = match;
|
||||||
|
if (embeddedBy.includes(messageID)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedChannel = ChannelStore.getChannel(channelID);
|
||||||
|
if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let linkedMessage = messageCache.get(messageID)?.message;
|
||||||
|
if (!linkedMessage) {
|
||||||
|
linkedMessage ??= MessageStore.getMessage(channelID, messageID);
|
||||||
|
if (linkedMessage) {
|
||||||
|
messageCache.set(messageID, { message: linkedMessage, fetched: true });
|
||||||
|
} else {
|
||||||
|
const msg = { ...message } as any;
|
||||||
|
delete msg.embeds;
|
||||||
|
messageFetchQueue.push(() => fetchMessage(channelID, messageID)
|
||||||
|
.then(m => m && FluxDispatcher.dispatch({
|
||||||
|
type: "MESSAGE_UPDATE",
|
||||||
|
message: msg
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageProps: MessageEmbedProps = {
|
||||||
|
message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),
|
||||||
|
channel: linkedChannel,
|
||||||
|
guildID
|
||||||
|
};
|
||||||
|
|
||||||
|
const type = settings.store.automodEmbeds;
|
||||||
|
accessories.push(
|
||||||
|
type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage))
|
||||||
|
? <AutomodEmbedAccessory {...messageProps} />
|
||||||
|
: <ChannelMessageEmbedAccessory {...messageProps} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessories.length ? <>{accessories}</> : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbedProps): JSX.Element | null {
|
||||||
|
const isDM = guildID === "@me";
|
||||||
|
|
||||||
|
const guild = !isDM && GuildStore.getGuild(channel.guild_id);
|
||||||
|
const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]);
|
||||||
|
|
||||||
|
|
||||||
|
return <Embed
|
||||||
|
embed={{
|
||||||
|
rawDescription: "",
|
||||||
|
color: "var(--background-secondary)",
|
||||||
|
author: {
|
||||||
|
name: <Text variant="text-xs/medium" tag="span">
|
||||||
|
<span>{isDM ? "Direct Message - " : (guild as Guild).name + " - "}</span>,
|
||||||
|
{isDM
|
||||||
|
? Parser.parse(`<@${dmReceiver.id}>`)
|
||||||
|
: Parser.parse(`<#${channel.id}>`)
|
||||||
|
}
|
||||||
|
</Text>,
|
||||||
|
iconProxyURL: guild
|
||||||
|
? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png`
|
||||||
|
: `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}`
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderDescription={() => (
|
||||||
|
<div key={message.id} className={classes(SearchResultClasses.message, settings.store.messageBackgroundColor && SearchResultClasses.searchResult)}>
|
||||||
|
<ChannelMessage
|
||||||
|
id={`message-link-embeds-${message.id}`}
|
||||||
|
message={message}
|
||||||
|
channel={channel}
|
||||||
|
subscribeToComponentDispatch={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
||||||
|
const { message, channel, guildID } = props;
|
||||||
|
|
||||||
|
const isDM = guildID === "@me";
|
||||||
|
const images = getImages(message);
|
||||||
|
const { parse } = Parser;
|
||||||
|
|
||||||
|
return <AutoModEmbed
|
||||||
|
channel={channel}
|
||||||
|
childrenAccessories={
|
||||||
|
<Text color="text-muted" variant="text-xs/medium" tag="span">
|
||||||
|
{isDM
|
||||||
|
? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`)
|
||||||
|
: parse(`<#${channel.id}>`)
|
||||||
|
},
|
||||||
|
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
compact={false}
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
{message.content || message.attachments.length <= images.length
|
||||||
|
? parse(message.content)
|
||||||
|
: [noContent(message.attachments.length, message.embeds.length)]
|
||||||
|
}
|
||||||
|
{images.map(a => {
|
||||||
|
const { width, height } = computeWidthAndHeight(a.width, a.height);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<img src={a.url} width={width} height={height} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
hideTimestamp={false}
|
||||||
|
message={message}
|
||||||
|
_messageEmbed="automod"
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessageLinkEmbeds",
|
name: "MessageLinkEmbeds",
|
||||||
description: "Adds a preview to messages that link another message",
|
description: "Adds a preview to messages that link another message",
|
||||||
authors: [Devs.TheSun],
|
authors: [Devs.TheSun, Devs.Ven],
|
||||||
dependencies: ["MessageAccessoriesAPI"],
|
dependencies: ["MessageAccessoriesAPI"],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".embedCard",
|
find: ".embedCard",
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: /{"use strict";(.{0,10})\(\)=>(.{1,2})}\);/,
|
match: /function (\i)\(\i\){var \i=\i\.message,\i=\i\.channel.{0,200}\.hideTimestamp/,
|
||||||
replace: '{"use strict";$1()=>$2,me:()=>messageEmbed});'
|
replace: "$self.AutoModEmbed=$1;$&"
|
||||||
}, {
|
|
||||||
match: /function (.{1,2})\(.{1,2}\){var .{1,2}=.{1,2}\.message,.{1,2}=.{1,2}\.channel.{0,300}\.embedCard.{0,500}}\)}/,
|
|
||||||
replace: "$&;var messageEmbed={mle_AutomodEmbed:$1};"
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
options: {
|
|
||||||
messageBackgroundColor: {
|
set AutoModEmbed(e: any) {
|
||||||
description: "Background color for messages in rich embeds",
|
AutoModEmbed = e;
|
||||||
type: OptionType.BOOLEAN
|
|
||||||
},
|
|
||||||
automodEmbeds: {
|
|
||||||
description: "Use automod embeds instead of rich embeds (smaller but less info)",
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
options: [{
|
|
||||||
label: "Always use automod embeds",
|
|
||||||
value: "always"
|
|
||||||
}, {
|
|
||||||
label: "Prefer automod embeds, but use rich embeds if some content can't be shown",
|
|
||||||
value: "prefer"
|
|
||||||
}, {
|
|
||||||
label: "Never use automod embeds",
|
|
||||||
value: "never",
|
|
||||||
default: true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
clearMessageCache: {
|
|
||||||
type: OptionType.COMPONENT,
|
|
||||||
description: "Clear the linked message cache",
|
|
||||||
component: () =>
|
|
||||||
<Button onClick={() => messageCache = {}}>
|
|
||||||
Clear the linked message cache
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
addAccessory("messageLinkEmbed", props => this.messageEmbedAccessory(props), 4 /* just above rich embeds*/);
|
addAccessory("messageLinkEmbed", props => {
|
||||||
},
|
if (!messageLinkRegex.test(props.message.content))
|
||||||
|
return null;
|
||||||
|
|
||||||
messageLinkRegex: /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,19}|@me)\/(\d{17,19})\/(\d{17,19})/g,
|
// need to reset the regex because it's global
|
||||||
|
messageLinkRegex.lastIndex = 0;
|
||||||
|
|
||||||
messageEmbedAccessory(props) {
|
return (
|
||||||
const { message }: { message: Message; } = props;
|
<ErrorBoundary>
|
||||||
// @ts-ignore
|
<MessageEmbedAccessory message={props.message} />
|
||||||
const embeddedBy: string[] = message.vencordEmbeddedBy ?? [];
|
</ErrorBoundary>
|
||||||
|
|
||||||
const accessories = [] as (JSX.Element | null)[];
|
|
||||||
|
|
||||||
let match = null as RegExpMatchArray | null;
|
|
||||||
while ((match = this.messageLinkRegex.exec(message.content!)) !== null) {
|
|
||||||
const [_, guildID, channelID, messageID] = match;
|
|
||||||
if (embeddedBy.includes(messageID)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkedChannel = ChannelStore.getChannel(channelID);
|
|
||||||
if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let linkedMessage = messageCache[messageID]?.message;
|
|
||||||
if (!linkedMessage) {
|
|
||||||
linkedMessage ??= MessageStore.getMessage(channelID, messageID);
|
|
||||||
if (linkedMessage) messageCache[messageID] = { message: linkedMessage, fetched: true };
|
|
||||||
else {
|
|
||||||
const msg = { ...message } as any;
|
|
||||||
delete msg.embeds;
|
|
||||||
messageFetchQueue.push(() => fetchMessage(channelID, messageID)
|
|
||||||
.then(m => m && FluxDispatcher.dispatch({
|
|
||||||
type: "MESSAGE_UPDATE",
|
|
||||||
message: msg
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const messageProps: MessageEmbedProps = {
|
|
||||||
message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),
|
|
||||||
channel: linkedChannel,
|
|
||||||
guildID
|
|
||||||
};
|
|
||||||
|
|
||||||
const type = Settings.plugins[this.name].automodEmbeds;
|
|
||||||
accessories.push(
|
|
||||||
type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage))
|
|
||||||
? this.automodEmbedAccessory(messageProps)
|
|
||||||
: this.channelMessageEmbedAccessory(messageProps)
|
|
||||||
);
|
);
|
||||||
}
|
}, 4 /* just above rich embeds */);
|
||||||
return accessories;
|
|
||||||
},
|
|
||||||
|
|
||||||
channelMessageEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
|
||||||
const { message, channel, guildID } = props;
|
|
||||||
|
|
||||||
const isDM = guildID === "@me";
|
|
||||||
const guild = !isDM && GuildStore.getGuild(channel.guild_id);
|
|
||||||
const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]);
|
|
||||||
const classNames = [SearchResultClasses.message];
|
|
||||||
if (Settings.plugins[this.name].messageBackgroundColor) classNames.push(SearchResultClasses.searchResult);
|
|
||||||
|
|
||||||
return <Embed
|
|
||||||
embed={{
|
|
||||||
rawDescription: "",
|
|
||||||
color: "var(--background-secondary)",
|
|
||||||
author: {
|
|
||||||
name: <Text variant="text-xs/medium" tag="span">
|
|
||||||
{[
|
|
||||||
<span>{isDM ? "Direct Message - " : (guild as Guild).name + " - "}</span>,
|
|
||||||
...(isDM
|
|
||||||
? Parser.parse(`<@${dmReceiver.id}>`)
|
|
||||||
: Parser.parse(`<#${channel.id}>`)
|
|
||||||
)
|
|
||||||
]}
|
|
||||||
</Text>,
|
|
||||||
iconProxyURL: guild
|
|
||||||
? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png`
|
|
||||||
: `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}`
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
renderDescription={() => {
|
|
||||||
return <div key={message.id} className={classNames.join(" ")}>
|
|
||||||
<ChannelMessage
|
|
||||||
id={`message-link-embeds-${message.id}`}
|
|
||||||
message={message}
|
|
||||||
channel={channel}
|
|
||||||
subscribeToComponentDispatch={false}
|
|
||||||
/>
|
|
||||||
</div >;
|
|
||||||
}}
|
|
||||||
/>;
|
|
||||||
},
|
|
||||||
|
|
||||||
automodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
|
||||||
const { message, channel, guildID } = props;
|
|
||||||
|
|
||||||
const isDM = guildID === "@me";
|
|
||||||
const images = getImages(message);
|
|
||||||
const { parse } = Parser;
|
|
||||||
|
|
||||||
return <AutomodEmbed
|
|
||||||
channel={channel}
|
|
||||||
childrenAccessories={<Text color="text-muted" variant="text-xs/medium" tag="span">
|
|
||||||
{[
|
|
||||||
...(isDM ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) : parse(`<#${channel.id}>`)),
|
|
||||||
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
|
|
||||||
]}
|
|
||||||
</Text>}
|
|
||||||
compact={false}
|
|
||||||
content={[
|
|
||||||
...(message.content || !(message.attachments.length > images.length)
|
|
||||||
? parse(message.content)
|
|
||||||
: [noContent(message.attachments.length, message.embeds.length)]
|
|
||||||
),
|
|
||||||
...(images.map<JSX.Element>(a => {
|
|
||||||
const { width, height } = computeWidthAndHeight(a.width, a.height);
|
|
||||||
return <div><img src={a.url} width={width} height={height} /></div>;
|
|
||||||
}
|
|
||||||
))
|
|
||||||
]}
|
|
||||||
hideTimestamp={false}
|
|
||||||
message={message}
|
|
||||||
_messageEmbed="automod"
|
|
||||||
/>;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
.messagelogger-deleted-attachment {
|
.messagelogger-deleted-attachment {
|
||||||
filter: grayscale(1);
|
filter: grayscale(1);
|
||||||
|
transition: 150ms filter ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messagelogger-deleted-attachment:hover {
|
.messagelogger-deleted-attachment:hover {
|
||||||
filter: grayscale(0);
|
filter: grayscale(0);
|
||||||
transition: 250ms filter linear;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-dark .messagelogger-edited {
|
.theme-dark .messagelogger-edited {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
import { definePluginSettings } from "@api/settings";
|
||||||
import { makeRange } from "@components/PluginSettings/components/SettingSliderComponent";
|
import { makeRange } from "@components/PluginSettings/components/SettingSliderComponent";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { sleep } from "@utils/misc";
|
import { sleep } from "@utils/misc";
|
||||||
@ -54,15 +54,36 @@ const MOYAI = "🗿";
|
|||||||
const MOYAI_URL =
|
const MOYAI_URL =
|
||||||
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3";
|
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
volume: {
|
||||||
|
description: "Volume of the 🗿🗿🗿",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
markers: makeRange(0, 1, 0.1),
|
||||||
|
default: 0.5,
|
||||||
|
stickToMarkers: false
|
||||||
|
},
|
||||||
|
triggerWhenUnfocused: {
|
||||||
|
description: "Trigger the 🗿 even when the window is unfocused",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
ignoreBots: {
|
||||||
|
description: "Ignore bots",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Moyai",
|
name: "Moyai",
|
||||||
authors: [Devs.Megu, Devs.Nuckyz],
|
authors: [Devs.Megu, Devs.Nuckyz],
|
||||||
description: "🗿🗿🗿🗿🗿🗿🗿🗿",
|
description: "🗿🗿🗿🗿🗿🗿🗿🗿",
|
||||||
|
settings,
|
||||||
|
|
||||||
async onMessage(e: IMessageCreate) {
|
async onMessage(e: IMessageCreate) {
|
||||||
if (e.optimistic || e.type !== "MESSAGE_CREATE") return;
|
if (e.optimistic || e.type !== "MESSAGE_CREATE") return;
|
||||||
if (e.message.state === "SENDING") return;
|
if (e.message.state === "SENDING") return;
|
||||||
if (Settings.plugins.Moyai.ignoreBots && e.message.author?.bot) return;
|
if (settings.store.ignoreBots && e.message.author?.bot) return;
|
||||||
if (!e.message.content) return;
|
if (!e.message.content) return;
|
||||||
if (e.channelId !== SelectedChannelStore.getChannelId()) return;
|
if (e.channelId !== SelectedChannelStore.getChannelId()) return;
|
||||||
|
|
||||||
@ -76,7 +97,7 @@ export default definePlugin({
|
|||||||
|
|
||||||
onReaction(e: IReactionAdd) {
|
onReaction(e: IReactionAdd) {
|
||||||
if (e.optimistic || e.type !== "MESSAGE_REACTION_ADD") return;
|
if (e.optimistic || e.type !== "MESSAGE_REACTION_ADD") return;
|
||||||
if (Settings.plugins.Moyai.ignoreBots && UserStore.getUser(e.userId)?.bot) return;
|
if (settings.store.ignoreBots && UserStore.getUser(e.userId)?.bot) return;
|
||||||
if (e.channelId !== SelectedChannelStore.getChannelId()) return;
|
if (e.channelId !== SelectedChannelStore.getChannelId()) return;
|
||||||
|
|
||||||
const name = e.emoji.name.toLowerCase();
|
const name = e.emoji.name.toLowerCase();
|
||||||
@ -103,28 +124,6 @@ export default definePlugin({
|
|||||||
FluxDispatcher.unsubscribe("MESSAGE_CREATE", this.onMessage);
|
FluxDispatcher.unsubscribe("MESSAGE_CREATE", this.onMessage);
|
||||||
FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD", this.onReaction);
|
FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD", this.onReaction);
|
||||||
FluxDispatcher.unsubscribe("VOICE_CHANNEL_EFFECT_SEND", this.onVoiceChannelEffect);
|
FluxDispatcher.unsubscribe("VOICE_CHANNEL_EFFECT_SEND", this.onVoiceChannelEffect);
|
||||||
},
|
|
||||||
|
|
||||||
options: {
|
|
||||||
volume: {
|
|
||||||
description: "Volume of the 🗿🗿🗿",
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
markers: makeRange(0, 1, 0.1),
|
|
||||||
default: 0.5,
|
|
||||||
stickToMarkers: false,
|
|
||||||
},
|
|
||||||
triggerWhenUnfocused: {
|
|
||||||
description: "Trigger the 🗿 even when the window is unfocused",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: false,
|
|
||||||
},
|
|
||||||
ignoreBots: {
|
|
||||||
description: "Ignore bots",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -158,9 +157,9 @@ function getMoyaiCount(message: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function boom() {
|
function boom() {
|
||||||
if (!Settings.plugins.Moyai.triggerWhenUnfocused && !document.hasFocus()) return;
|
if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return;
|
||||||
const audioElement = document.createElement("audio");
|
const audioElement = document.createElement("audio");
|
||||||
audioElement.src = MOYAI_URL;
|
audioElement.src = MOYAI_URL;
|
||||||
audioElement.volume = Settings.plugins.Moyai.volume;
|
audioElement.volume = settings.store.volume;
|
||||||
audioElement.play();
|
audioElement.play();
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ migratePluginSettings("NoDevtoolsWarning", "STFU");
|
|||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoDevtoolsWarning",
|
name: "NoDevtoolsWarning",
|
||||||
description: "Disables the 'HOLD UP' banner in the console",
|
description: "Disables the 'HOLD UP' banner in the console. As a side effect, also prevents Discord from hiding your token, which prevents random logouts.",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
patches: [{
|
patches: [{
|
||||||
find: "setDevtoolsCallbacks",
|
find: "setDevtoolsCallbacks",
|
||||||
|
@ -16,11 +16,13 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { migratePluginSettings } from "@api/settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
migratePluginSettings("NoF1", "No F1");
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "No F1",
|
name: "NoF1",
|
||||||
description: "Disables F1 help bind.",
|
description: "Disables F1 help bind.",
|
||||||
authors: [Devs.Cyn],
|
authors: [Devs.Cyn],
|
||||||
patches: [
|
patches: [
|
||||||
|
@ -16,14 +16,15 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { migratePluginSettings } from "@api/settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
migratePluginSettings("NoRPC", "No RPC");
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "No RPC",
|
name: "NoRPC",
|
||||||
description: "Disables Discord's RPC server.",
|
description: "Disables Discord's RPC server.",
|
||||||
authors: [Devs.Cyn],
|
authors: [Devs.Cyn],
|
||||||
target: "DESKTOP",
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '.ensureModule("discord_rpc")',
|
find: '.ensureModule("discord_rpc")',
|
@ -27,12 +27,12 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: '("ApplicationStreamPreviewUploadManager")',
|
find: '("ApplicationStreamPreviewUploadManager")',
|
||||||
replacement: [
|
replacement: [
|
||||||
".\\.default\\.makeChunkedRequest\\(",
|
"\\i\\.default\\.makeChunkedRequest\\(",
|
||||||
".{1,2}\\..\\.post\\({url:"
|
"\\i\\.\\i\\.post\\({url:"
|
||||||
].map(match => ({
|
].map(match => ({
|
||||||
match: new RegExp(`return\\[(?<code>\\d),${match}.\\..{1,3}\\.STREAM_PREVIEW.+?}\\)\\];`),
|
match: new RegExp(`(?=return\\[(\\d),${match}\\i\\.\\i\\.STREAM_PREVIEW.+?}\\)\\];)`),
|
||||||
replace: 'return[$<code>,Promise.resolve({body:"",status:204})];'
|
replace: (_, code) => `return[${code},Promise.resolve({body:"",status:204})];`
|
||||||
}))
|
}))
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
|
@ -23,7 +23,6 @@ export default definePlugin({
|
|||||||
name: "NoSystemBadge",
|
name: "NoSystemBadge",
|
||||||
description: "Disables the taskbar and system tray unread count badge.",
|
description: "Disables the taskbar and system tray unread count badge.",
|
||||||
authors: [Devs.rushii],
|
authors: [Devs.rushii],
|
||||||
target: "DESKTOP",
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "setSystemTrayApplications:function",
|
find: "setSystemTrayApplications:function",
|
@ -38,6 +38,19 @@ export default definePlugin({
|
|||||||
match: /window\.DiscordSentry=function.+\}\(\)/,
|
match: /window\.DiscordSentry=function.+\}\(\)/,
|
||||||
replace: "",
|
replace: "",
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".METRICS,",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /this\._intervalId.+?12e4\)/,
|
||||||
|
replace: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(?<=increment=function\(\i\){)/,
|
||||||
|
replace: "return;"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -41,7 +41,7 @@ export default definePlugin({
|
|||||||
replace: "[$1, $self.PronounsChatComponent(e)]"
|
replace: "[$1, $self.PronounsChatComponent(e)]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Hijack the discord pronouns section (hidden without experiment) and add a wrapper around the text section
|
// Hijack the discord pronouns section and add a wrapper around the text section
|
||||||
{
|
{
|
||||||
find: ".Messages.BOT_PROFILE_SLASH_COMMANDS",
|
find: ".Messages.BOT_PROFILE_SLASH_COMMANDS",
|
||||||
replacement: {
|
replacement: {
|
||||||
@ -49,12 +49,12 @@ export default definePlugin({
|
|||||||
replace: "$<fullProps>&&$self.PronounsProfileWrapper($<PronounComponent>,$<pronounProps>,$<fullProps>)"
|
replace: "$<fullProps>&&$self.PronounsProfileWrapper($<PronounComponent>,$<pronounProps>,$<fullProps>)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Make pronouns experiment be enabled by default
|
// Force enable pronouns component ignoring the experiment value
|
||||||
{
|
{
|
||||||
find: "2022-01_pronouns",
|
find: ".Messages.USER_POPOUT_PRONOUNS",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: "!1", // false
|
match: /\i\.\i\.useExperiment\({}\)\.showPronouns/,
|
||||||
replace: "!0"
|
replace: "true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -30,10 +30,10 @@ export default definePlugin({
|
|||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".revealSpoiler=function",
|
find: ".removeObscurity=function",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\.revealSpoiler=function\((.{1,2})\){/,
|
match: /(?<=\.removeObscurity=function\((\i)\){)/,
|
||||||
replace: ".revealSpoiler=function($1){$self.reveal($1);"
|
replace: (_, event) => `$self.reveal(${event});`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Menu } from "@webpack/common";
|
import { Menu } from "@webpack/common";
|
||||||
@ -29,39 +30,21 @@ const Engines = {
|
|||||||
ImgOps: "https://imgops.com/start?url="
|
ImgOps: "https://imgops.com/start?url="
|
||||||
};
|
};
|
||||||
|
|
||||||
export default definePlugin({
|
function search(src: string, engine: string) {
|
||||||
name: "ReverseImageSearch",
|
open(engine + encodeURIComponent(src), "_blank");
|
||||||
description: "Adds ImageSearch to image context menus",
|
}
|
||||||
authors: [Devs.Ven],
|
|
||||||
dependencies: ["MenuItemDeobfuscatorAPI"],
|
|
||||||
patches: [{
|
|
||||||
find: "open-native-link",
|
|
||||||
replacement: {
|
|
||||||
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
|
|
||||||
replace: (m, src) =>
|
|
||||||
`${m},Vencord.Plugins.plugins.ReverseImageSearch.makeMenu(${src}, arguments[2])`
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
// pass the target to the open link menu so we can check if it's an image
|
|
||||||
find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /ariaLabel:\i\.Z\.Messages\.MESSAGE_ACTIONS_MENU_LABEL/,
|
|
||||||
replace: "$&,_vencordTarget:arguments[0].target"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// var f = props.itemHref, .... MakeNativeMenu(null != f ? f : blah)
|
|
||||||
match: /(\i)=\i\.itemHref,.+?\(null!=\1\?\1:.{1,10}(?=\))/,
|
|
||||||
replace: "$&,arguments[0]._vencordTarget"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
|
|
||||||
makeMenu(src: string, target: HTMLElement) {
|
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, args) => {
|
||||||
if (target && !(target instanceof HTMLImageElement) && target.attributes["data-role"]?.value !== "img")
|
if (!args?.[0]) return;
|
||||||
return null;
|
const { reverseImageSearchType, itemHref, itemSrc } = args[0];
|
||||||
|
|
||||||
return (
|
if (!reverseImageSearchType || reverseImageSearchType !== "img") return;
|
||||||
|
|
||||||
|
const src = itemHref ?? itemSrc;
|
||||||
|
|
||||||
|
const group = findGroupChildrenByChildId("save-image", children);
|
||||||
|
if (group && !group.some(child => child?.props?.id === "search-image")) {
|
||||||
|
group.push((
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
label="Search Image"
|
label="Search Image"
|
||||||
key="search-image"
|
key="search-image"
|
||||||
@ -74,7 +57,7 @@ export default definePlugin({
|
|||||||
key={key}
|
key={key}
|
||||||
id={key}
|
id={key}
|
||||||
label={engine}
|
label={engine}
|
||||||
action={() => this.search(src, Engines[engine])}
|
action={() => search(src, Engines[engine])}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -82,14 +65,33 @@ export default definePlugin({
|
|||||||
key="search-image-all"
|
key="search-image-all"
|
||||||
id="search-image-all"
|
id="search-image-all"
|
||||||
label="All"
|
label="All"
|
||||||
action={() => Object.values(Engines).forEach(e => this.search(src, e))}
|
action={() => Object.values(Engines).forEach(e => search(src, e))}
|
||||||
/>
|
/>
|
||||||
</Menu.MenuItem>
|
</Menu.MenuItem>
|
||||||
);
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ReverseImageSearch",
|
||||||
|
description: "Adds ImageSearch to image context menus",
|
||||||
|
authors: [Devs.Ven, Devs.Nuckyz],
|
||||||
|
dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",
|
||||||
|
replacement: {
|
||||||
|
match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/,
|
||||||
|
replace: (m, target) => `${m}reverseImageSearchType:${target}.getAttribute("data-role"),`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
start() {
|
||||||
|
addContextMenuPatch("message", imageContextMenuPatch);
|
||||||
},
|
},
|
||||||
|
|
||||||
// openUrl is a mangled export, so just match it in the module and pass it
|
stop() {
|
||||||
search(src: string, engine: string) {
|
removeContextMenuPatch("message", imageContextMenuPatch);
|
||||||
open(engine + encodeURIComponent(src), "_blank");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
|
||||||
|
|
||||||
import { Review } from "../entities/Review";
|
|
||||||
import { authorize, showToast } from "./Utils";
|
|
||||||
|
|
||||||
const API_URL = "https://manti.vendicated.dev";
|
|
||||||
|
|
||||||
const getToken = () => Settings.plugins.ReviewDB.token;
|
|
||||||
|
|
||||||
enum Response {
|
|
||||||
"Added your review" = 0,
|
|
||||||
"Updated your review" = 1,
|
|
||||||
"Error" = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getReviews(id: string): Promise<Review[]> {
|
|
||||||
const res = await fetch(API_URL + "/getUserReviews?snowflakeFormat=string&discordid=" + id);
|
|
||||||
return await res.json() as Review[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addReview(review: any): Promise<Response> {
|
|
||||||
review.token = getToken();
|
|
||||||
|
|
||||||
if (!review.token) {
|
|
||||||
showToast("Please authorize to add a review.");
|
|
||||||
authorize();
|
|
||||||
return Response.Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch(API_URL + "/addUserReview", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(review),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(res => {
|
|
||||||
showToast(res);
|
|
||||||
return Response[res] ?? Response.Error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteReview(id: number): Promise<any> {
|
|
||||||
return fetch(API_URL + "/deleteReview", {
|
|
||||||
method: "POST",
|
|
||||||
headers: new Headers({
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json",
|
|
||||||
}),
|
|
||||||
body: JSON.stringify({
|
|
||||||
token: getToken(),
|
|
||||||
reviewid: id
|
|
||||||
})
|
|
||||||
}).then(r => r.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function reportReview(id: number) {
|
|
||||||
const res = await fetch(API_URL + "/reportReview", {
|
|
||||||
method: "POST",
|
|
||||||
headers: new Headers({
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json",
|
|
||||||
}),
|
|
||||||
body: JSON.stringify({
|
|
||||||
reviewid: id,
|
|
||||||
token: getToken()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
showToast(await res.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLastReviewID(id: string): Promise<number> {
|
|
||||||
return fetch(API_URL + "/getLastReviewID?discordid=" + id)
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(Number);
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { openModal } from "@utils/modal";
|
|
||||||
import { findByProps } from "@webpack";
|
|
||||||
import { FluxDispatcher, React, SelectedChannelStore, Toasts, UserUtils } from "@webpack/common";
|
|
||||||
|
|
||||||
import { Review } from "../entities/Review";
|
|
||||||
|
|
||||||
export async function openUserProfileModal(userId: string) {
|
|
||||||
await UserUtils.fetchUser(userId);
|
|
||||||
|
|
||||||
await FluxDispatcher.dispatch({
|
|
||||||
type: "USER_PROFILE_MODAL_OPEN",
|
|
||||||
userId,
|
|
||||||
channelId: SelectedChannelStore.getChannelId(),
|
|
||||||
analyticsLocation: "Explosive Hotel"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function authorize(callback?: any) {
|
|
||||||
const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal");
|
|
||||||
|
|
||||||
openModal((props: any) =>
|
|
||||||
<OAuth2AuthorizeModal
|
|
||||||
{...props}
|
|
||||||
scopes={["identify"]}
|
|
||||||
responseType="code"
|
|
||||||
redirectUri="https://manti.vendicated.dev/URauth"
|
|
||||||
permissions={0n}
|
|
||||||
clientId="915703782174752809"
|
|
||||||
cancelCompletesFlow={false}
|
|
||||||
callback={async (u: string) => {
|
|
||||||
try {
|
|
||||||
const url = new URL(u);
|
|
||||||
url.searchParams.append("returnType", "json");
|
|
||||||
url.searchParams.append("clientMod", "vencord");
|
|
||||||
const res = await fetch(url, {
|
|
||||||
headers: new Headers({ Accept: "application/json" })
|
|
||||||
});
|
|
||||||
const { token, status } = await res.json();
|
|
||||||
if (status === 0) {
|
|
||||||
Settings.plugins.ReviewDB.token = token;
|
|
||||||
showToast("Successfully logged in!");
|
|
||||||
callback?.();
|
|
||||||
} else if (res.status === 1) {
|
|
||||||
showToast("An Error occurred while logging in.");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
new Logger("ReviewDB").error("Failed to authorise", e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function showToast(text: string) {
|
|
||||||
Toasts.show({
|
|
||||||
type: Toasts.Type.MESSAGE,
|
|
||||||
message: text,
|
|
||||||
id: Toasts.genId(),
|
|
||||||
options: {
|
|
||||||
position: Toasts.Position.BOTTOM
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
|
|
||||||
|
|
||||||
export function canDeleteReview(review: Review, userId: string) {
|
|
||||||
if (review.senderdiscordid === userId) return true;
|
|
||||||
|
|
||||||
const myId = BigInt(userId);
|
|
||||||
return myId === Devs.mantikafasi.id ||
|
|
||||||
myId === Devs.Ven.id ||
|
|
||||||
myId === Devs.rushii.id;
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { classes, LazyComponent } from "@utils/misc";
|
|
||||||
import { findByProps } from "@webpack";
|
|
||||||
|
|
||||||
export default LazyComponent(() => {
|
|
||||||
const { button, dangerous } = findByProps("button", "wrapper", "disabled");
|
|
||||||
|
|
||||||
return function MessageButton(props) {
|
|
||||||
return props.type === "delete"
|
|
||||||
? (
|
|
||||||
<div className={classes(button, dangerous)} aria-label="Delete Review" onClick={props.callback}>
|
|
||||||
<svg aria-hidden="false" width="16" height="16" viewBox="0 0 20 20">
|
|
||||||
<path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"></path>
|
|
||||||
<path fill="currentColor" d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<div className={button} aria-label="Report Review" onClick={() => props.callback()}>
|
|
||||||
<svg aria-hidden="false" width="16" height="16" viewBox="0 0 20 20">
|
|
||||||
<path fill="currentColor" d="M20,6.002H14V3.002C14,2.45 13.553,2.002 13,2.002H4C3.447,2.002 3,2.45 3,3.002V22.002H5V14.002H10.586L8.293,16.295C8.007,16.581 7.922,17.011 8.076,17.385C8.23,17.759 8.596,18.002 9,18.002H20C20.553,18.002 21,17.554 21,17.002V7.002C21,6.45 20.553,6.002 20,6.002Z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MaskedLinkStore, Tooltip } from "@webpack/common";
|
|
||||||
|
|
||||||
import { Badge } from "../entities/Badge";
|
|
||||||
|
|
||||||
export default function ReviewBadge(badge: Badge) {
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
text={badge.badge_name}>
|
|
||||||
{({ onMouseEnter, onMouseLeave }) => (
|
|
||||||
<img
|
|
||||||
width="24px"
|
|
||||||
height="24px"
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
src={badge.badge_icon}
|
|
||||||
alt={badge.badge_description}
|
|
||||||
style={{ verticalAlign: "middle", marginLeft: "4px" }}
|
|
||||||
onClick={() =>
|
|
||||||
MaskedLinkStore.openUntrustedLink({
|
|
||||||
href: badge.redirect_url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,125 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { classes, LazyComponent } from "@utils/misc";
|
|
||||||
import { filters, findBulk } from "@webpack";
|
|
||||||
import { Alerts, UserStore } from "@webpack/common";
|
|
||||||
|
|
||||||
import { Review } from "../entities/Review";
|
|
||||||
import { deleteReview, reportReview } from "../Utils/ReviewDBAPI";
|
|
||||||
import { canDeleteReview, openUserProfileModal, showToast } from "../Utils/Utils";
|
|
||||||
import MessageButton from "./MessageButton";
|
|
||||||
import ReviewBadge from "./ReviewBadge";
|
|
||||||
|
|
||||||
export default LazyComponent(() => {
|
|
||||||
// this is terrible, blame mantika
|
|
||||||
const p = filters.byProps;
|
|
||||||
const [
|
|
||||||
{ cozyMessage, buttons, message, groupStart },
|
|
||||||
{ container, isHeader },
|
|
||||||
{ avatar, clickable, username, messageContent, wrapper, cozy },
|
|
||||||
{ contents },
|
|
||||||
buttonClasses,
|
|
||||||
{ defaultColor }
|
|
||||||
] = findBulk(
|
|
||||||
p("cozyMessage"),
|
|
||||||
p("container", "isHeader"),
|
|
||||||
p("avatar", "zalgo"),
|
|
||||||
p("contents"),
|
|
||||||
p("button", "wrapper", "disabled"),
|
|
||||||
p("defaultColor")
|
|
||||||
);
|
|
||||||
|
|
||||||
return function ReviewComponent({ review, refetch }: { review: Review; refetch(): void; }) {
|
|
||||||
function openModal() {
|
|
||||||
openUserProfileModal(review.senderdiscordid);
|
|
||||||
}
|
|
||||||
|
|
||||||
function delReview() {
|
|
||||||
Alerts.show({
|
|
||||||
title: "Are you sure?",
|
|
||||||
body: "Do you really want to delete this review?",
|
|
||||||
confirmText: "Delete",
|
|
||||||
cancelText: "Nevermind",
|
|
||||||
onConfirm: () => {
|
|
||||||
deleteReview(review.id).then(res => {
|
|
||||||
if (res.successful) {
|
|
||||||
refetch();
|
|
||||||
}
|
|
||||||
showToast(res.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportRev() {
|
|
||||||
Alerts.show({
|
|
||||||
title: "Are you sure?",
|
|
||||||
body: "Do you really you want to report this review?",
|
|
||||||
confirmText: "Report",
|
|
||||||
cancelText: "Nevermind",
|
|
||||||
// confirmColor: "red", this just adds a class name and breaks the submit button guh
|
|
||||||
onConfirm: () => reportReview(review.id)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes(cozyMessage, wrapper, message, groupStart, cozy, "user-review")} style={
|
|
||||||
{
|
|
||||||
marginLeft: "0px",
|
|
||||||
paddingLeft: "52px",
|
|
||||||
paddingRight: "16px"
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
|
|
||||||
<div className={contents} style={{ paddingLeft: "0px" }}>
|
|
||||||
<img
|
|
||||||
className={classes(avatar, clickable)}
|
|
||||||
onClick={openModal}
|
|
||||||
src={review.profile_photo || "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"}
|
|
||||||
style={{ left: "0px" }}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={classes(clickable, username)}
|
|
||||||
style={{ color: "var(--channels-default)", fontSize: "14px" }}
|
|
||||||
onClick={() => openModal()}
|
|
||||||
>
|
|
||||||
{review.username}
|
|
||||||
</span>
|
|
||||||
{review.badges.map(badge => <ReviewBadge {...badge} />)}
|
|
||||||
<p
|
|
||||||
className={classes(messageContent, defaultColor)}
|
|
||||||
style={{ fontSize: 15, marginTop: 4 }}
|
|
||||||
>
|
|
||||||
{review.comment}
|
|
||||||
</p>
|
|
||||||
<div className={classes(container, isHeader, buttons)} style={{
|
|
||||||
padding: "0px",
|
|
||||||
}}>
|
|
||||||
<div className={buttonClasses.wrapper} >
|
|
||||||
<MessageButton type="report" callback={reportRev} />
|
|
||||||
{canDeleteReview(review, UserStore.getCurrentUser().id) && (
|
|
||||||
<MessageButton type="delete" callback={delReview} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { classes, useAwaiter } from "@utils/misc";
|
|
||||||
import { findLazy } from "@webpack";
|
|
||||||
import { Forms, React, Text, UserStore } from "@webpack/common";
|
|
||||||
import type { KeyboardEvent } from "react";
|
|
||||||
|
|
||||||
import { addReview, getReviews } from "../Utils/ReviewDBAPI";
|
|
||||||
import ReviewComponent from "./ReviewComponent";
|
|
||||||
|
|
||||||
const Classes = findLazy(m => typeof m.textarea === "string");
|
|
||||||
|
|
||||||
export default function ReviewsView({ userId }: { userId: string; }) {
|
|
||||||
const [refetchCount, setRefetchCount] = React.useState(0);
|
|
||||||
const [reviews, _, isLoading] = useAwaiter(() => getReviews(userId), {
|
|
||||||
fallbackValue: [],
|
|
||||||
deps: [refetchCount],
|
|
||||||
});
|
|
||||||
const username = UserStore.getUser(userId)?.username ?? "";
|
|
||||||
|
|
||||||
const dirtyRefetch = () => setRefetchCount(refetchCount + 1);
|
|
||||||
|
|
||||||
if (isLoading) return null;
|
|
||||||
|
|
||||||
function onKeyPress({ key, target }: KeyboardEvent<HTMLTextAreaElement>) {
|
|
||||||
if (key === "Enter") {
|
|
||||||
addReview({
|
|
||||||
userid: userId,
|
|
||||||
comment: (target as HTMLInputElement).value,
|
|
||||||
star: -1
|
|
||||||
}).then(res => {
|
|
||||||
if (res === 0 || res === 1) {
|
|
||||||
(target as HTMLInputElement).value = ""; // clear the input
|
|
||||||
dirtyRefetch();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ReviewDB">
|
|
||||||
<Text
|
|
||||||
tag="h2"
|
|
||||||
variant="eyebrow"
|
|
||||||
style={{
|
|
||||||
marginBottom: "12px",
|
|
||||||
color: "var(--header-primary)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
User Reviews
|
|
||||||
</Text>
|
|
||||||
{reviews?.map(review =>
|
|
||||||
<ReviewComponent
|
|
||||||
key={review.id}
|
|
||||||
review={review}
|
|
||||||
refetch={dirtyRefetch}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{reviews?.length === 0 && (
|
|
||||||
<Forms.FormText style={{ padding: "12px", paddingTop: "0px", paddingLeft: "4px", fontWeight: "bold", fontStyle: "italic" }}>
|
|
||||||
Looks like nobody reviewed this user yet. You could be the first!
|
|
||||||
</Forms.FormText>
|
|
||||||
)}
|
|
||||||
<textarea
|
|
||||||
className={classes(Classes.textarea.replace("textarea", ""), "enter-comment")}
|
|
||||||
// this produces something like '-_59yqs ...' but since no class exists with that name its fine
|
|
||||||
placeholder={reviews?.some(r => r.senderdiscordid === UserStore.getCurrentUser().id) ? `Update review for @${username}` : `Review @${username}`}
|
|
||||||
onKeyDown={onKeyPress}
|
|
||||||
style={{
|
|
||||||
marginTop: "6px",
|
|
||||||
resize: "none",
|
|
||||||
marginBottom: "12px",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Badge } from "./Badge";
|
|
||||||
|
|
||||||
export interface Review {
|
|
||||||
comment: string,
|
|
||||||
id: number,
|
|
||||||
senderdiscordid: string,
|
|
||||||
senderuserid: number,
|
|
||||||
star: number,
|
|
||||||
username: string,
|
|
||||||
profile_photo: string;
|
|
||||||
badges: Badge[];
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { Button, UserStore } from "@webpack/common";
|
|
||||||
import { User } from "discord-types/general";
|
|
||||||
|
|
||||||
import ReviewsView from "./components/ReviewsView";
|
|
||||||
import { getLastReviewID } from "./Utils/ReviewDBAPI";
|
|
||||||
import { authorize, showToast } from "./Utils/Utils";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "ReviewDB",
|
|
||||||
description: "Review other users (Adds a new settings to profiles)",
|
|
||||||
authors: [Devs.mantikafasi, Devs.Ven],
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "disableBorderColor:!0",
|
|
||||||
replacement: {
|
|
||||||
match: /\(.{0,10}\{user:(.),setNote:.,canDM:.,.+?\}\)/,
|
|
||||||
replace: "$&,$self.getReviewsComponent($1)"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
options: {
|
|
||||||
authorize: {
|
|
||||||
type: OptionType.COMPONENT,
|
|
||||||
description: "Authorise with ReviewDB",
|
|
||||||
component: () => (
|
|
||||||
<Button onClick={authorize}>
|
|
||||||
Authorise with ReviewDB
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
notifyReviews: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Notify about new reviews on startup",
|
|
||||||
default: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
const settings = Settings.plugins.ReviewDB;
|
|
||||||
if (!settings.lastReviewId || !settings.notifyReviews) return;
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
const id = await getLastReviewID(UserStore.getCurrentUser().id);
|
|
||||||
if (settings.lastReviewId < id) {
|
|
||||||
showToast("You have new reviews on your profile!");
|
|
||||||
settings.lastReviewId = id;
|
|
||||||
}
|
|
||||||
}, 4000);
|
|
||||||
},
|
|
||||||
|
|
||||||
getReviewsComponent: (user: User) => (
|
|
||||||
<ErrorBoundary message="Failed to render Reviews">
|
|
||||||
<ReviewsView userId={user.id} />
|
|
||||||
</ErrorBoundary>
|
|
||||||
)
|
|
||||||
});
|
|
@ -1,67 +1,67 @@
|
|||||||
/*
|
/*
|
||||||
* Vencord, a modification for Discord's desktop app
|
* Vencord, a modification for Discord's desktop app
|
||||||
* Copyright (c) 2022 OpenAsar
|
* Copyright (c) 2022 OpenAsar
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Forms } from "@webpack/common";
|
import { Forms } from "@webpack/common";
|
||||||
const appIds = [
|
const appIds = [
|
||||||
"911790844204437504",
|
"911790844204437504",
|
||||||
"886578863147192350",
|
"886578863147192350",
|
||||||
"1020414178047041627",
|
"1020414178047041627",
|
||||||
"1032800329332445255"
|
"1032800329332445255"
|
||||||
];
|
];
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "richerCider",
|
name: "richerCider",
|
||||||
description: "Enhances Cider (More details in info button) by adding the \"Listening to\" type prefix to the user's rich presence when an applicable ID is found.",
|
description: "Enhances Cider (More details in info button) by adding the \"Listening to\" type prefix to the user's rich presence when an applicable ID is found.",
|
||||||
authors: [{
|
authors: [{
|
||||||
id: 191621342473224192n,
|
id: 191621342473224192n,
|
||||||
name: "cryptofyre",
|
name: "cryptofyre",
|
||||||
}],
|
}],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '.displayName="LocalActivityStore"',
|
find: '.displayName="LocalActivityStore"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /LOCAL_ACTIVITY_UPDATE:function\((\i)\)\{/,
|
match: /LOCAL_ACTIVITY_UPDATE:function\((\i)\)\{/,
|
||||||
replace: "$&$self.patchActivity($1.activity);",
|
replace: "$&$self.patchActivity($1.activity);",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle tag="h3">Install Cider to use this Plugin</Forms.FormTitle>
|
<Forms.FormTitle tag="h3">Install Cider to use this Plugin</Forms.FormTitle>
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
<Link href="https://cider.sh">Follow the link to our website</Link> to get Cider up and running, and then enable the plugin.
|
<Link href="https://cider.sh">Follow the link to our website</Link> to get Cider up and running, and then enable the plugin.
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<br></br>
|
<br></br>
|
||||||
<Forms.FormTitle tag="h3">What is Cider?</Forms.FormTitle>
|
<Forms.FormTitle tag="h3">What is Cider?</Forms.FormTitle>
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
Cider is an open-source and community oriented Apple Music client for Windows, macOS, and Linux.
|
Cider is an open-source and community oriented Apple Music client for Windows, macOS, and Linux.
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<br></br>
|
<br></br>
|
||||||
<Forms.FormTitle tag="h3">Recommended Optional Plugins</Forms.FormTitle>
|
<Forms.FormTitle tag="h3">Recommended Optional Plugins</Forms.FormTitle>
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
I'd recommend using TimeBarAllActivities alongside this plugin to give off a much better visual to the eye (Keep in mind this only affects your client and will not show for other users)
|
I'd recommend using TimeBarAllActivities alongside this plugin to give off a much better visual to the eye (Keep in mind this only affects your client and will not show for other users)
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
patchActivity(activity: any) {
|
patchActivity(activity: any) {
|
||||||
if (appIds.includes(activity.application_id)) {
|
if (appIds.includes(activity.application_id)) {
|
||||||
activity.type = 2; /* LISTENING type */
|
activity.type = 2; /* LISTENING type */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
@ -19,9 +19,11 @@
|
|||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { LazyComponent } from "@utils/misc";
|
import { LazyComponent } from "@utils/misc";
|
||||||
import { formatDuration } from "@utils/text";
|
import { formatDuration } from "@utils/text";
|
||||||
import { find, findByCode, findByPropsLazy } from "@webpack";
|
import { find, findByPropsLazy } from "@webpack";
|
||||||
import { FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common";
|
import { FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common";
|
||||||
import { Channel } from "discord-types/general";
|
import { Channel } from "discord-types/general";
|
||||||
|
import type { ComponentType } from "react";
|
||||||
|
|
||||||
|
|
||||||
enum SortOrderTypes {
|
enum SortOrderTypes {
|
||||||
LATEST_ACTIVITY = 0,
|
LATEST_ACTIVITY = 0,
|
||||||
@ -73,6 +75,17 @@ enum ChannelFlags {
|
|||||||
REQUIRE_TAG = 1 << 4
|
REQUIRE_TAG = 1 << 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let EmojiComponent: ComponentType<any>;
|
||||||
|
let ChannelBeginHeader: ComponentType<any>;
|
||||||
|
|
||||||
|
export function setEmojiComponent(component: ComponentType<any>) {
|
||||||
|
EmojiComponent = component;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setChannelBeginHeaderComponent(component: ComponentType<any>) {
|
||||||
|
ChannelBeginHeader = component;
|
||||||
|
}
|
||||||
|
|
||||||
const ChatScrollClasses = findByPropsLazy("auto", "content", "scrollerBase");
|
const ChatScrollClasses = findByPropsLazy("auto", "content", "scrollerBase");
|
||||||
const TagComponent = LazyComponent(() => find(m => {
|
const TagComponent = LazyComponent(() => find(m => {
|
||||||
if (typeof m !== "function") return false;
|
if (typeof m !== "function") return false;
|
||||||
@ -81,9 +94,6 @@ const TagComponent = LazyComponent(() => find(m => {
|
|||||||
// Get the component which doesn't include increasedActivity logic
|
// Get the component which doesn't include increasedActivity logic
|
||||||
return code.includes(".Messages.FORUM_TAG_A11Y_FILTER_BY_TAG") && !code.includes("increasedActivityPill");
|
return code.includes(".Messages.FORUM_TAG_A11Y_FILTER_BY_TAG") && !code.includes("increasedActivityPill");
|
||||||
}));
|
}));
|
||||||
const EmojiComponent = LazyComponent(() => findByCode('.jumboable?"jumbo":"default"'));
|
|
||||||
// The component for the beggining of a channel, but we patched it so it only returns the allowed users and roles components for hidden channels
|
|
||||||
const ChannelBeginHeader = LazyComponent(() => findByCode(".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE"));
|
|
||||||
|
|
||||||
const ChannelTypesToChannelNames = {
|
const ChannelTypesToChannelNames = {
|
||||||
[ChannelTypes.GUILD_TEXT]: "text",
|
[ChannelTypes.GUILD_TEXT]: "text",
|
||||||
|
@ -26,7 +26,7 @@ import { findByPropsLazy } from "@webpack";
|
|||||||
import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common";
|
import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common";
|
||||||
import { Channel } from "discord-types/general";
|
import { Channel } from "discord-types/general";
|
||||||
|
|
||||||
import HiddenChannelLockScreen from "./components/HiddenChannelLockScreen";
|
import HiddenChannelLockScreen, { setChannelBeginHeaderComponent, setEmojiComponent } from "./components/HiddenChannelLockScreen";
|
||||||
|
|
||||||
const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer");
|
const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer");
|
||||||
|
|
||||||
@ -64,29 +64,29 @@ export default definePlugin({
|
|||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
// RenderLevel defines if a channel is hidden, collapsed in category, visible, etc
|
// RenderLevel defines if a channel is hidden, collapsed in category, visible, etc
|
||||||
find: ".CannotShow",
|
find: ".CannotShow=",
|
||||||
// These replacements only change the necessary CannotShow's
|
// These replacements only change the necessary CannotShow's
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=isChannelGatedAndVisible\(this\.record\.guild_id,this\.record\.id\).+?renderLevel:)(?<RenderLevels>\i)\..+?(?=,)/,
|
match: /(?<=isChannelGatedAndVisible\(this\.record\.guild_id,this\.record\.id\).+?renderLevel:)(\i)\..+?(?=,)/,
|
||||||
replace: "this.category.isCollapsed?$<RenderLevels>.WouldShowIfUncollapsed:$<RenderLevels>.Show"
|
replace: (_, RenderLevels) => `this.category.isCollapsed?${RenderLevels}.WouldShowIfUncollapsed:${RenderLevels}.Show`
|
||||||
},
|
},
|
||||||
// Move isChannelGatedAndVisible renderLevel logic to the bottom to not show hidden channels in case they are muted
|
// Move isChannelGatedAndVisible renderLevel logic to the bottom to not show hidden channels in case they are muted
|
||||||
{
|
{
|
||||||
match: /(?<=(?<permissionCheck>if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{)if\(this\.id===\i\).+?};)(?<isChannelGatedAndVisibleCondition>if\(!\i\.\i\.isChannelGatedAndVisible\(.+?})(?<restOfFunction>.+?)(?=return{renderLevel:\i\.Show.{1,40}return \i)/,
|
match: /(?<=(if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{)if\(this\.id===\i\).+?};)(if\(!\i\.\i\.isChannelGatedAndVisible\(.+?})(.+?)(?=return{renderLevel:\i\.Show.{0,40}?return \i)/,
|
||||||
replace: "$<restOfFunction>$<permissionCheck>$<isChannelGatedAndVisibleCondition>}"
|
replace: (_, permissionCheck, isChannelGatedAndVisibleCondition, rest) => `${rest}${permissionCheck}${isChannelGatedAndVisibleCondition}}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=renderLevel:(?<renderLevelExpression>\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/,
|
match: /(?<=renderLevel:(\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/,
|
||||||
replace: "$<renderLevelExpression>"
|
replace: (_, renderLevelExpression) => renderLevelExpression
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(?<RenderLevels>\i)\..+?(?=,)/,
|
match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(\i)\..+?(?=,)/,
|
||||||
replace: "$<RenderLevels>.Show"
|
replace: (_, RenderLevels) => `${RenderLevels}.Show`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=getRenderLevel=function.+?return ).+?\?(?<renderLevelExpressionWithoutPermCheck>.+?):\i\.CannotShow(?=})/,
|
match: /(?<=getRenderLevel=function.+?return ).+?\?(.+?):\i\.CannotShow(?=})/,
|
||||||
replace: "$<renderLevelExpressionWithoutPermCheck>"
|
replace: (_, renderLevelExpressionWithoutPermCheck) => renderLevelExpressionWithoutPermCheck
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -95,18 +95,18 @@ export default definePlugin({
|
|||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Do not show confirmation to join a voice channel when already connected to another if clicking on a hidden voice channel
|
// Do not show confirmation to join a voice channel when already connected to another if clicking on a hidden voice channel
|
||||||
match: /(?<=getCurrentClientVoiceChannelId\(\i\.guild_id\);if\()(?=.+?\((?<channel>\i)\))/,
|
match: /(?<=getCurrentClientVoiceChannelId\((\i)\.guild_id\);if\()/,
|
||||||
replace: "!$self.isHiddenChannel($<channel>)&&"
|
replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Make Discord think we are connected to a voice channel so it shows us inside it
|
// Make Discord think we are connected to a voice channel so it shows us inside it
|
||||||
match: /(?=\|\|\i\.default\.selectVoiceChannel\((?<channel>\i)\.id\))/,
|
match: /(?=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\))/,
|
||||||
replace: "||$self.isHiddenChannel($<channel>)"
|
replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Make Discord think we are connected to a voice channel so it shows us inside it
|
// Make Discord think we are connected to a voice channel so it shows us inside it
|
||||||
match: /(?<=\|\|\i\.default\.selectVoiceChannel\((?<channel>\i)\.id\);!__OVERLAY__&&\()/,
|
match: /(?<=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\);!__OVERLAY__&&\()/,
|
||||||
replace: "$self.isHiddenChannel($<channel>)||"
|
replace: (_, channel) => `$self.isHiddenChannel(${channel})||`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -119,7 +119,7 @@ export default definePlugin({
|
|||||||
"renderInviteButton",
|
"renderInviteButton",
|
||||||
"renderOpenChatButton"
|
"renderOpenChatButton"
|
||||||
].map(func => ({
|
].map(func => ({
|
||||||
match: new RegExp(`(?<=\\i\\.${func}=function\\(\\){)`, "g"), // Global because Discord has multiple declarations of the same functions
|
match: new RegExp(`(?<=${func}=function\\(\\){)`, "g"), // Global because Discord has multiple declarations of the same functions
|
||||||
replace: "if($self.isHiddenChannel(this.props.channel))return null;"
|
replace: "if($self.isHiddenChannel(this.props.channel))return null;"
|
||||||
}))
|
}))
|
||||||
]
|
]
|
||||||
@ -129,17 +129,8 @@ export default definePlugin({
|
|||||||
predicate: () => settings.store.showMode === ShowMode.LockIcon,
|
predicate: () => settings.store.showMode === ShowMode.LockIcon,
|
||||||
replacement: {
|
replacement: {
|
||||||
// Lock Icon
|
// Lock Icon
|
||||||
match: /(?=switch\((?<channel>\i)\.type\).{1,30}\.GUILD_ANNOUNCEMENT.{1,30}\(0,\i\.\i\))/,
|
match: /(?=switch\((\i)\.type\).{0,30}\.GUILD_ANNOUNCEMENT.{0,30}\(0,\i\.\i\))/,
|
||||||
replace: "if($self.isHiddenChannel($<channel>))return $self.LockIcon;"
|
replace: (_, channel) => `if($self.isHiddenChannel(${channel}))return $self.LockIcon;`
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: ".UNREAD_HIGHLIGHT",
|
|
||||||
predicate: () => settings.store.hideUnreads === true,
|
|
||||||
replacement: {
|
|
||||||
// Hide unreads
|
|
||||||
match: /(?<=\i\.connected,\i=)(?=(?<props>\i)\.unread)/,
|
|
||||||
replace: "$self.isHiddenChannel($<props>.channel)?false:"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -148,36 +139,44 @@ export default definePlugin({
|
|||||||
replacement: [
|
replacement: [
|
||||||
// Make the channel appear as muted if it's hidden
|
// Make the channel appear as muted if it's hidden
|
||||||
{
|
{
|
||||||
match: /(?<=\i\.name,\i=)(?=(?<props>\i)\.muted)/,
|
match: /(?<=\i\.name,\i=)(?=(\i)\.muted)/,
|
||||||
replace: "$self.isHiddenChannel($<props>.channel)?true:"
|
replace: (_, props) => `$self.isHiddenChannel(${props}.channel)?true:`
|
||||||
},
|
},
|
||||||
// Add the hidden eye icon if the channel is hidden
|
// Add the hidden eye icon if the channel is hidden
|
||||||
{
|
{
|
||||||
match: /(?<=(?<channel>\i)=\i\.channel,.+?\(\)\.children.+?:null)/,
|
match: /\(\).children.+?:null(?<=(\i)=\i\.channel,.+?)/,
|
||||||
replace: ",$self.isHiddenChannel($<channel>)?$self.HiddenChannelIcon():null"
|
replace: (m, channel) => `${m},$self.isHiddenChannel(${channel})?$self.HiddenChannelIcon():null`
|
||||||
},
|
},
|
||||||
// Make voice channels also appear as muted if they are muted
|
// Make voice channels also appear as muted if they are muted
|
||||||
{
|
{
|
||||||
match: /(?<=\i\(\)\.wrapper:\i\(\)\.notInteractive,)(?<otherClasses>.+?)(?<mutedClassExpression>(?<isMuted>\i)\?\i\.MUTED)/,
|
match: /(?<=\.wrapper:\i\(\)\.notInteractive,)(.+?)((\i)\?\i\.MUTED)/,
|
||||||
replace: "$<mutedClassExpression>:\"\",$<otherClasses>$<isMuted>?\"\""
|
replace: (_, otherClasses, mutedClassExpression, isMuted) => `${mutedClassExpression}:"",${otherClasses}${isMuted}?""`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden
|
|
||||||
{
|
{
|
||||||
find: ".UNREAD_HIGHLIGHT",
|
find: ".UNREAD_HIGHLIGHT",
|
||||||
predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,
|
replacement: [
|
||||||
replacement: {
|
{
|
||||||
match: /(?<=(?<channel>\i)=\i\.channel,.+?\.LOCKED:\i)/,
|
// Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden
|
||||||
replace: "&&!($self.settings.store.hideUnreads===false&&$self.isHiddenChannel($<channel>))"
|
predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,
|
||||||
}
|
match: /\.LOCKED:\i(?<=(\i)=\i\.channel,.+?)/,
|
||||||
|
replace: (m, channel) => `${m}&&!$self.isHiddenChannel(${channel})`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Hide unreads
|
||||||
|
predicate: () => settings.store.hideUnreads === true,
|
||||||
|
match: /(?<=\i\.connected,\i=)(?=(\i)\.unread)/,
|
||||||
|
replace: (_, props) => `$self.isHiddenChannel(${props}.channel)?false:`
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Hide New unreads box for hidden channels
|
// Hide New unreads box for hidden channels
|
||||||
find: '.displayName="ChannelListUnreadsStore"',
|
find: '.displayName="ChannelListUnreadsStore"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=return null!=(?<channel>\i))(?=.{1,130}hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module
|
match: /(?<=return null!=(\i))(?=.{0,130}?hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module
|
||||||
replace: "&&!$self.isHiddenChannel($<channel>)"
|
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Only render the channel header and buttons that work when transitioning to a hidden channel
|
// Only render the channel header and buttons that work when transitioning to a hidden channel
|
||||||
@ -185,20 +184,20 @@ export default definePlugin({
|
|||||||
find: "Missing channel in Channel.renderHeaderToolbar",
|
find: "Missing channel in Channel.renderHeaderToolbar",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_TEXT:)(?=.+?;(?<pushNotificationButtonExpression>.+?{channel:(?<channel>\i)},"notifications"\)\);))/,
|
match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_TEXT:)(?=.+?;(.+?{channel:(\i)},"notifications"\)\);))/,
|
||||||
replace: "if($self.isHiddenChannel($<channel>)){$<pushNotificationButtonExpression>break;}"
|
replace: (_, pushNotificationButtonExpression, channel) => `if($self.isHiddenChannel(${channel})){${pushNotificationButtonExpression}break;}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_FORUM:if\(!\i\){)(?=.+?;(?<pushNotificationButtonExpression>.+?{channel:(?<channel>\i)},"notifications"\)\)))/,
|
match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_FORUM:if\(!\i\){)(?=.+?;(.+?{channel:(\i)},"notifications"\)\)))/,
|
||||||
replace: "if($self.isHiddenChannel($<channel>)){$<pushNotificationButtonExpression>;break;}"
|
replace: (_, pushNotificationButtonExpression, channel) => `if($self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=(?<this>\i)\.renderMobileToolbar=function.+?case \i\.\i\.GUILD_FORUM:)/,
|
match: /renderMobileToolbar=function.+?case \i\.\i\.GUILD_FORUM:(?<=(\i)\.renderMobileToolbar.+?)/,
|
||||||
replace: "if($self.isHiddenChannel($<this>.props.channel))break;"
|
replace: (m, that) => `${m}if($self.isHiddenChannel(${that}.props.channel))break;`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=renderHeaderBar=function.+?hideSearch:(?<channel>\i)\.isDirectory\(\))/,
|
match: /(?<=renderHeaderBar=function.+?hideSearch:(\i)\.isDirectory\(\))/,
|
||||||
replace: "||$self.isHiddenChannel($<channel>)"
|
replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=renderSidebar=function\(\){)/,
|
match: /(?<=renderSidebar=function\(\){)/,
|
||||||
@ -213,25 +212,23 @@ export default definePlugin({
|
|||||||
// Avoid trying to fetch messages from hidden channels
|
// Avoid trying to fetch messages from hidden channels
|
||||||
{
|
{
|
||||||
find: '"MessageManager"',
|
find: '"MessageManager"',
|
||||||
replacement: [
|
replacement: {
|
||||||
{
|
match: /"Skipping fetch because channelId is a static route"\);else{(?=.+?getChannel\((\i)\))/,
|
||||||
match: /(?<=if\(null!=(?<channelId>\i)\).{1,100}"Skipping fetch because channelId is a static route".{1,10}else{)/,
|
replace: (m, channelId) => `${m}if($self.isHiddenChannel({channelId:${channelId}}))return;`
|
||||||
replace: "if($self.isHiddenChannel({channelId:$<channelId>}))return;"
|
}
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
// Patch keybind handlers so you can't accidentally jump to hidden channels
|
// Patch keybind handlers so you can't accidentally jump to hidden channels
|
||||||
{
|
{
|
||||||
find: '"alt+shift+down"',
|
find: '"alt+shift+down"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=getChannel\(\i\);return null!=(?<channel>\i))(?=.{1,130}hasRelevantUnread\(\i\))/,
|
match: /(?<=getChannel\(\i\);return null!=(\i))(?=.{0,130}?hasRelevantUnread\(\i\))/,
|
||||||
replace: "&&!$self.isHiddenChannel($<channel>)"
|
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '"alt+down"',
|
find: '"alt+down"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=getState\(\)\.channelId.{1,30}\(0,\i\.\i\)\(\i\))(?=\.map\()/,
|
match: /(?<=getState\(\)\.channelId.{0,30}?\(0,\i\.\i\)\(\i\))(?=\.map\()/,
|
||||||
replace: ".filter(ch=>!$self.isHiddenChannel(ch))"
|
replace: ".filter(ch=>!$self.isHiddenChannel(ch))"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -239,8 +236,8 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: 'jumboable?"jumbo":"default"',
|
find: 'jumboable?"jumbo":"default"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=\i:\(\)=>\i)(?=}.+?(?<component>\i)=function.{1,20}node,\i=\i.isInteracting)/,
|
match: /jumboable\?"jumbo":"default",emojiId.+?}}\)},(?<=(\i)=function\(\i\){var \i=\i\.node.+?)/,
|
||||||
replace: ",hc1:()=>$<component>" // Blame Ven length check for the small name :pensive_cry:
|
replace: (m, component) => `${m}shcEmojiComponentExport=($self.setEmojiComponent(${component}),void 0),`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -248,13 +245,13 @@ export default definePlugin({
|
|||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Export the channel beggining header
|
// Export the channel beggining header
|
||||||
match: /(?<=\i:\(\)=>\i)(?=}.+?function (?<component>\i).{1,600}computePermissionsForRoles)/,
|
match: /computePermissionsForRoles.+?}\)}(?<=function (\i)\(.+?)(?=var)/,
|
||||||
replace: ",hc2:()=>$<component>"
|
replace: (m, component) => `${m}$self.setChannelBeginHeaderComponent(${component});`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Patch the header to only return allowed users and roles if it's a hidden channel (Like when it's used on the HiddenChannelLockScreen)
|
// Patch the header to only return allowed users and roles if it's a hidden channel (Like when it's used on the HiddenChannelLockScreen)
|
||||||
match: /(?<=MANAGE_ROLES.{1,60}return)(?=\(.+?(?<component>\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(?<channel>\i)\.guild_id.+?roleColor.+?]}\)))/,
|
match: /MANAGE_ROLES.{0,60}?return(?=\(.+?(\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(\i)\.guild_id.+?roleColor.+?]}\)))/,
|
||||||
replace: " $self.isHiddenChannel($<channel>)?$<component>:"
|
replace: (m, component, channel) => `${m} $self.isHiddenChannel(${channel})?${component}:`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -263,23 +260,23 @@ export default definePlugin({
|
|||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Remove the divider and the open chat button for the HiddenChannelLockScreen
|
// Remove the divider and the open chat button for the HiddenChannelLockScreen
|
||||||
match: /(?<=function \i\((?<props>\i)\).{1,2000}"more-options-popout"\)\);if\()/,
|
match: /"more-options-popout"\)\);if\((?<=function \i\((\i)\).+?)/,
|
||||||
replace: "(!$self.isHiddenChannel($<props>.channel)||$<props>.inCall)&&"
|
replace: (m, props) => `${m}(!$self.isHiddenChannel(${props}.channel)||${props}.inCall)&&`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Render our HiddenChannelLockScreen component instead of the main voice channel component
|
// Render our HiddenChannelLockScreen component instead of the main voice channel component
|
||||||
match: /(?<=renderContent=function.{1,1700}children:)/,
|
match: /this\.renderVoiceChannelEffects.+?children:(?<=renderContent=function.+?)/,
|
||||||
replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?$self.HiddenChannelLockScreen(this.props.channel):"
|
replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?$self.HiddenChannelLockScreen(this.props.channel):"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Disable gradients for the HiddenChannelLockScreen of voice channels
|
// Disable gradients for the HiddenChannelLockScreen of voice channels
|
||||||
match: /(?<=renderContent=function.{1,1600}disableGradients:)/,
|
match: /this\.renderVoiceChannelEffects.+?disableGradients:(?<=renderContent=function.+?)/,
|
||||||
replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)||"
|
replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel)||"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Disable useless components for the HiddenChannelLockScreen of voice channels
|
// Disable useless components for the HiddenChannelLockScreen of voice channels
|
||||||
match: /(?<=renderContent=function.{1,800}render(?!Header).{0,30}:)(?!void)/g,
|
match: /(?:{|,)render(?!Header|ExternalHeader).{0,30}?:(?<=renderContent=function.+?)(?!void)/g,
|
||||||
replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?null:"
|
replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?null:"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -288,43 +285,64 @@ export default definePlugin({
|
|||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Render our HiddenChannelLockScreen component instead of the main stage channel component
|
// Render our HiddenChannelLockScreen component instead of the main stage channel component
|
||||||
match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,1400}children:)(?=.{1,20}}\)}function)/,
|
match: /Guild voice channel without guild id.+?children:(?<=(\i)\.getGuildId\(\).+?)(?=.{0,20}?}\)}function)/,
|
||||||
replace: "$self.isHiddenChannel($<channel>)?$self.HiddenChannelLockScreen($<channel>):"
|
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?$self.HiddenChannelLockScreen(${channel}):`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Disable useless components for the HiddenChannelLockScreen of stage channels
|
// Disable useless components for the HiddenChannelLockScreen of stage channels
|
||||||
match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,1000}render(?!Header).{0,30}:)/g,
|
match: /render(?!Header).{0,30}?:(?<=(\i)\.getGuildId\(\).+?Guild voice channel without guild id.+?)/g,
|
||||||
replace: "$self.isHiddenChannel($<channel>)?null:"
|
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?null:`
|
||||||
},
|
},
|
||||||
// Prevent Discord from replacing our route if we aren't connected to the stage channel
|
// Prevent Discord from replacing our route if we aren't connected to the stage channel
|
||||||
{
|
{
|
||||||
match: /(?<=if\()(?=!\i&&!\i&&!\i.{1,80}(?<channel>\i)\.getGuildId\(\).{1,50}Guild voice channel without guild id\.)/,
|
match: /(?=!\i&&!\i&&!\i.{0,80}?(\i)\.getGuildId\(\).{0,50}?Guild voice channel without guild id)(?<=if\()/,
|
||||||
replace: "!$self.isHiddenChannel($<channel>)&&"
|
replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Disable gradients for the HiddenChannelLockScreen of stage channels
|
// Disable gradients for the HiddenChannelLockScreen of stage channels
|
||||||
match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,600}disableGradients:)/,
|
match: /Guild voice channel without guild id.+?disableGradients:(?<=(\i)\.getGuildId\(\).+?)/,
|
||||||
replace: "$self.isHiddenChannel($<channel>)||"
|
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})||`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Disable strange styles applied to the header for the HiddenChannelLockScreen of stage channels
|
// Disable strange styles applied to the header for the HiddenChannelLockScreen of stage channels
|
||||||
match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,600}style:)/,
|
match: /Guild voice channel without guild id.+?style:(?<=(\i)\.getGuildId\(\).+?)/,
|
||||||
replace: "$self.isHiddenChannel($<channel>)?undefined:"
|
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?undefined:`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Remove the divider and amount of users in stage channel components for the HiddenChannelLockScreen
|
// Remove the divider and amount of users in stage channel components for the HiddenChannelLockScreen
|
||||||
match: /\(0,\i\.jsx\)\(\i\.\i\.Divider.+?}\)]}\)(?=.+?:(?<channel>\i)\.guild_id)/,
|
match: /\(0,\i\.jsx\)\(\i\.\i\.Divider.+?}\)]}\)(?=.+?:(\i)\.guild_id)/,
|
||||||
replace: "$self.isHiddenChannel($<channel>)?null:($&)"
|
replace: (m, channel) => `$self.isHiddenChannel(${channel})?null:(${m})`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Remove the open chat button for the HiddenChannelLockScreen
|
// Remove the open chat button for the HiddenChannelLockScreen
|
||||||
match: /(?<=null,)(?=.{1,120}channelId:(?<channel>\i)\.id,.+?toggleRequestToSpeakSidebar:\i,iconClassName:\i\(\)\.buttonIcon)/,
|
match: /"recents".+?null,(?=.{0,120}?channelId:(\i)\.id)/,
|
||||||
replace: "!$self.isHiddenChannel($<channel>)&&"
|
replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The module wasn't being found, so lets just escape everything
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
find: "\^https\:\/\/\(\?\:canary\.\|ptb\.\)\?discord.com\/channels\/\(\\\\\d\+\|",
|
||||||
|
replacement: {
|
||||||
|
// Make mentions of hidden channels work
|
||||||
|
match: /\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,\i\)/,
|
||||||
|
replace: "true"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".shouldCloseDefaultModals",
|
||||||
|
replacement: {
|
||||||
|
// Show inside voice channel instead of trying to join them when clicking on a channel mention
|
||||||
|
match: /(?<=getChannel\((\i)\)\)(?=.{0,100}?selectVoiceChannel))/,
|
||||||
|
replace: (_, channelId) => `&&!$self.isHiddenChannel({channelId:${channelId}})`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
setEmojiComponent,
|
||||||
|
setChannelBeginHeaderComponent,
|
||||||
|
|
||||||
isHiddenChannel(channel: Channel & { channelId?: string; }) {
|
isHiddenChannel(channel: Channel & { channelId?: string; }) {
|
||||||
if (!channel) return false;
|
if (!channel) return false;
|
||||||
|
|
||||||
|
85
src/plugins/silentMessageToggle.tsx
Normal file
85
src/plugins/silentMessageToggle.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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 { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { Button, ButtonLooks, ButtonWrapperClasses, React, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
|
function SilentMessageToggle() {
|
||||||
|
const [enabled, setEnabled] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const listener: SendListener = (_, message) => {
|
||||||
|
if (enabled) {
|
||||||
|
setEnabled(false);
|
||||||
|
if (!message.content.startsWith("@silent ")) message.content = "@silent " + message.content;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addPreSendListener(listener);
|
||||||
|
return () => void removePreSendListener(listener);
|
||||||
|
}, [enabled]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip text="Toggle Silent Message">
|
||||||
|
{tooltipProps => (
|
||||||
|
<Button
|
||||||
|
{...tooltipProps}
|
||||||
|
onClick={() => setEnabled(prev => !prev)}
|
||||||
|
size=""
|
||||||
|
look={ButtonLooks.BLANK}
|
||||||
|
innerClassName={ButtonWrapperClasses.button}
|
||||||
|
style={{ margin: "0px 8px" }}
|
||||||
|
>
|
||||||
|
<div className={ButtonWrapperClasses.buttonWrapper}>
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g fill="currentColor">
|
||||||
|
<path d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4C13 3.69264 13.0198 3.3899 13.0582 3.093C12.7147 3.03189 12.3611 3 12 3C8.686 3 6 5.686 6 9V14C6 15.657 4.656 17 3 17V18H21V17C19.344 17 18 15.657 18 14V10.7101ZM8.55493 19C9.24793 20.19 10.5239 21 11.9999 21C13.4759 21 14.7519 20.19 15.4449 19H8.55493Z" />
|
||||||
|
<path d="M18.2624 5.50209L21 2.5V1H16.0349V2.49791H18.476L16 5.61088V7H21V5.50209H18.2624Z" />
|
||||||
|
{!enabled && <line x1="22" y1="2" x2="2" y2="22" stroke="var(--red-500)" stroke-width="2.5" />}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "SilentMessageToggle",
|
||||||
|
authors: [Devs.Nuckyz],
|
||||||
|
description: "Adds a button to the chat bar to toggle sending a silent message.",
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".activeCommandOption",
|
||||||
|
replacement: {
|
||||||
|
match: /"gift"\)\);(?<=(\i)\.push.+?)/,
|
||||||
|
replace: (m, array) => `${m}${array}.push($self.SilentMessageToggle());`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
SilentMessageToggle: ErrorBoundary.wrap(SilentMessageToggle, { noop: true }),
|
||||||
|
});
|
@ -82,8 +82,8 @@ export default definePlugin({
|
|||||||
find: ".activeCommandOption",
|
find: ".activeCommandOption",
|
||||||
predicate: () => settings.store.showIcon,
|
predicate: () => settings.store.showIcon,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\i=\i\.activeCommand,\i=\i\.activeCommandOption,.{1,133}(.)=\[\];/,
|
match: /(.)\.push.{1,50}\(\i,\{.{1,30}\},"gift"\)\)/,
|
||||||
replace: "$&;$1.push($self.chatBarIcon());",
|
replace: "$&;try{$1.push($self.chatBarIcon())}catch{}",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -16,53 +16,55 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { migratePluginSettings, Settings } from "@api/settings";
|
import { definePluginSettings, migratePluginSettings } from "@api/settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
noSpotifyAutoPause: {
|
||||||
|
description: "Disable Spotify auto-pause",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
|
},
|
||||||
|
keepSpotifyActivityOnIdle: {
|
||||||
|
description: "Keep Spotify activity playing when idling",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false,
|
||||||
|
restartNeeded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
migratePluginSettings("SpotifyCrack", "Ify");
|
migratePluginSettings("SpotifyCrack", "Ify");
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "SpotifyCrack",
|
name: "SpotifyCrack",
|
||||||
description: "Free listen along, no auto-pausing in voice chat, and allows activity to continue playing when idling",
|
description: "Free listen along, no auto-pausing in voice chat, and allows activity to continue playing when idling",
|
||||||
authors: [
|
authors: [Devs.Cyn, Devs.Nuckyz],
|
||||||
Devs.Cyn,
|
settings,
|
||||||
Devs.Nuckyz
|
|
||||||
],
|
|
||||||
|
|
||||||
patches: [{
|
patches: [
|
||||||
find: 'dispatch({type:"SPOTIFY_PROFILE_UPDATE"',
|
{
|
||||||
replacement: [{
|
|
||||||
match: /(function\((.{1,2})\){)(.{1,6}dispatch\({type:"SPOTIFY_PROFILE_UPDATE")/,
|
|
||||||
replace: (_, functionStart, data, functionBody) => `${functionStart}${data}.body.product="premium";${functionBody}`
|
|
||||||
}],
|
|
||||||
}, {
|
|
||||||
find: '.displayName="SpotifyStore"',
|
|
||||||
predicate: () => Settings.plugins.SpotifyCrack.noSpotifyAutoPause,
|
|
||||||
replacement: {
|
|
||||||
match: /function (.{1,2})\(\).{0,200}SPOTIFY_AUTO_PAUSED\);.{0,}}}}/,
|
|
||||||
replace: "function $1(){}"
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
find: '.displayName="SpotifyStore"',
|
|
||||||
predicate: () => Settings.plugins.SpotifyCrack.keepSpotifyActivityOnIdle,
|
|
||||||
replacement: {
|
|
||||||
match: /(shouldShowActivity=function\(\){.{1,50})&&!.{1,6}\.isIdle\(\)(.{0,}?})/,
|
|
||||||
replace: (_, functionDeclarationAndExpression, restOfFunction) => `${functionDeclarationAndExpression}${restOfFunction}`
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
options: {
|
find: 'dispatch({type:"SPOTIFY_PROFILE_UPDATE"',
|
||||||
noSpotifyAutoPause: {
|
replacement: {
|
||||||
description: "Disable Spotify auto-pause",
|
match: /SPOTIFY_PROFILE_UPDATE.+?isPremium:(?="premium"===(\i)\.body\.product)/,
|
||||||
type: OptionType.BOOLEAN,
|
replace: (m, req) => `${m}(${req}.body.product="premium")&&`
|
||||||
default: true,
|
},
|
||||||
restartNeeded: true,
|
|
||||||
},
|
},
|
||||||
keepSpotifyActivityOnIdle: {
|
{
|
||||||
description: "Keep Spotify activity playing when idling",
|
find: '.displayName="SpotifyStore"',
|
||||||
type: OptionType.BOOLEAN,
|
replacement: [
|
||||||
default: false,
|
{
|
||||||
restartNeeded: true,
|
predicate: () => settings.store.noSpotifyAutoPause,
|
||||||
|
match: /(?<=function \i\(\){)(?=.{0,200}SPOTIFY_AUTO_PAUSED\))/,
|
||||||
|
replace: "return;"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
predicate: () => settings.store.keepSpotifyActivityOnIdle,
|
||||||
|
match: /(?<=shouldShowActivity=function\(\){.{0,50})&&!\i\.\i\.isIdle\(\)/,
|
||||||
|
replace: ""
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
});
|
});
|
||||||
|
91
src/plugins/supportHelper.tsx
Normal file
91
src/plugins/supportHelper.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
||||||
|
import { makeCodeblock } from "@utils/misc";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { isOutdated } from "@utils/updater";
|
||||||
|
import { Alerts, FluxDispatcher, Forms, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
|
import gitHash from "~git-hash";
|
||||||
|
import plugins from "~plugins";
|
||||||
|
|
||||||
|
import settings from "./settings";
|
||||||
|
|
||||||
|
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "SupportHelper",
|
||||||
|
required: true,
|
||||||
|
description: "Helps me provide support to you",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
|
commands: [{
|
||||||
|
name: "vencord-debug",
|
||||||
|
description: "Send Vencord Debug info",
|
||||||
|
predicate: ctx => ctx.channel.id === SUPPORT_CHANNEL_ID,
|
||||||
|
execute() {
|
||||||
|
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
|
||||||
|
|
||||||
|
const debugInfo = `
|
||||||
|
**Vencord Debug Info**
|
||||||
|
|
||||||
|
> Discord Branch: ${RELEASE_CHANNEL}
|
||||||
|
> Client: ${typeof DiscordNative === "undefined" ? window.armcord ? "Armcord" : `Web (${navigator.userAgent})` : `Desktop (Electron v${settings.electronVersion})`}
|
||||||
|
> Platform: ${window.navigator.platform}
|
||||||
|
> Vencord Version: ${gitHash}${settings.additionalInfo}
|
||||||
|
> Outdated: ${isOutdated}
|
||||||
|
> Enabled Plugins:
|
||||||
|
${makeCodeblock(Object.keys(plugins).filter(Vencord.Plugins.isPluginEnabled).join(", "))}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: debugInfo.trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
|
||||||
|
rememberDismiss() {
|
||||||
|
DataStore.set(REMEMBER_DISMISS_KEY, gitHash);
|
||||||
|
},
|
||||||
|
|
||||||
|
start() {
|
||||||
|
FluxDispatcher.subscribe("CHANNEL_SELECT", async ({ channelId }) => {
|
||||||
|
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
||||||
|
|
||||||
|
const myId = BigInt(UserStore.getCurrentUser().id);
|
||||||
|
if (Object.values(Devs).some(d => d.id === myId)) return;
|
||||||
|
|
||||||
|
if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) {
|
||||||
|
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>
|
||||||
|
</div>,
|
||||||
|
onCancel: this.rememberDismiss,
|
||||||
|
onConfirm: this.rememberDismiss
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -121,8 +121,8 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: ".UNREAD_HIGHLIGHT",
|
find: ".UNREAD_HIGHLIGHT",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=(?<channel>\i)=\i\.channel,.+?\(\)\.children.+?:null)/,
|
match: /\(\).children.+?:null(?<=(\i)=\i\.channel,.+?)/,
|
||||||
replace: ",$self.TypingIndicator($<channel>.id)"
|
replace: (m, channel) => `${m},$self.TypingIndicator(${channel}.id)`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -24,7 +24,7 @@ import { findByCodeLazy } from "@webpack";
|
|||||||
import { GuildMemberStore, React, RelationshipStore } from "@webpack/common";
|
import { GuildMemberStore, React, RelationshipStore } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
const Avatar = findByCodeLazy(".Positions.TOP,spacing:");
|
const Avatar = findByCodeLazy('"top",spacing:');
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
showAvatars: {
|
showAvatars: {
|
||||||
@ -70,7 +70,7 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: "getCooldownTextStyle",
|
find: "getCooldownTextStyle",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /return \i\.Z\.getName\(.,.\.props\.channel\.id,(.)\)/,
|
match: /return \i\.\i\.getName\(.,.\.props\.channel\.id,(.)\)/,
|
||||||
replace: "return $1"
|
replace: "return $1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -105,7 +105,7 @@ export default definePlugin({
|
|||||||
}}>
|
}}>
|
||||||
{settings.store.showAvatars && <div style={{ marginTop: "4px" }}>
|
{settings.store.showAvatars && <div style={{ marginTop: "4px" }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={Avatar.Sizes.SIZE_16}
|
size="SIZE_16"
|
||||||
src={user.getAvatarURL(guildId, 128)} />
|
src={user.getAvatarURL(guildId, 128)} />
|
||||||
</div>}
|
</div>}
|
||||||
{GuildMemberStore.getNick(guildId!, user.id) || !guildId && RelationshipStore.getNickname(user.id) || user.username}
|
{GuildMemberStore.getNick(guildId!, user.id) || !guildId && RelationshipStore.getNickname(user.id) || user.username}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
import { migratePluginSettings } from "@api/settings";
|
import { migratePluginSettings } from "@api/settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { SelectedChannelStore } from "@webpack/common";
|
import { ChannelStore, SelectedChannelStore } from "@webpack/common";
|
||||||
|
|
||||||
const timers = {} as Record<string, {
|
const timers = {} as Record<string, {
|
||||||
timeout?: NodeJS.Timeout;
|
timeout?: NodeJS.Timeout;
|
||||||
@ -48,14 +48,21 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// channel mentions
|
// channel mentions
|
||||||
find: ".EMOJI_IN_MESSAGE_HOVER",
|
find: ".shouldCloseDefaultModals",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /onClick:(\i)(?=,.{0,30}className:"channelMention")/,
|
match: /onClick:(\i)(?=,.{0,30}className:"channelMention".+?(\i)\.inContent)/,
|
||||||
replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)()",
|
replace: (_, onClick, props) => ""
|
||||||
|
+ `onClick:(vcDoubleClickEvt)=>$self.shouldRunOnClick(vcDoubleClickEvt,${props})&&${onClick}()`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
shouldRunOnClick(e: MouseEvent, { channelId }) {
|
||||||
|
const channel = ChannelStore.getChannel(channelId);
|
||||||
|
if (!channel || ![2, 13].includes(channel.type)) return true;
|
||||||
|
return e.detail >= 2;
|
||||||
|
},
|
||||||
|
|
||||||
schedule(cb: () => void, e: any) {
|
schedule(cb: () => void, e: any) {
|
||||||
const id = e.props.channel.id as string;
|
const id = e.props.channel.id as string;
|
||||||
if (SelectedChannelStore.getVoiceChannelId() === id) {
|
if (SelectedChannelStore.getVoiceChannelId() === id) {
|
||||||
|
@ -20,10 +20,11 @@ import { Settings } from "@api/settings";
|
|||||||
import { ErrorCard } from "@components/ErrorCard";
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { wordsToTitle } from "@utils/text";
|
import { wordsToTitle } from "@utils/text";
|
||||||
import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types";
|
import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Button, ChannelStore, FluxDispatcher, Forms, Margins, SelectedChannelStore, useMemo, UserStore } from "@webpack/common";
|
import { Button, ChannelStore, FluxDispatcher, Forms, SelectedChannelStore, useMemo, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
interface VoiceState {
|
interface VoiceState {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -304,7 +305,7 @@ export default definePlugin({
|
|||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
{hasEnglishVoices && (
|
{hasEnglishVoices && (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.marginTop20} tag="h3">Play Example Sounds</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20} tag="h3">Play Example Sounds</Forms.FormTitle>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
|
@ -20,10 +20,11 @@ import { addButton, removeButton } from "@api/MessagePopover";
|
|||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { copyWithToast } from "@utils/misc";
|
import { copyWithToast } from "@utils/misc";
|
||||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Button, ChannelStore, Forms, Margins, Parser, Text } from "@webpack/common";
|
import { Button, ChannelStore, Forms, Parser, Text } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
|
||||||
@ -98,7 +99,7 @@ function openViewRawModal(msg: Message) {
|
|||||||
<>
|
<>
|
||||||
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
|
||||||
<CodeBlock content={msg.content} lang="" />
|
<CodeBlock content={msg.content} lang="" />
|
||||||
<Forms.FormDivider className={Margins.marginBottom20} />
|
<Forms.FormDivider className={Margins.bottom20} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -16,14 +16,26 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { makeRange } from "@components/PluginSettings/components/SettingSliderComponent";
|
import { definePluginSettings } from "@api/settings";
|
||||||
|
import { makeRange } from "@components/PluginSettings/components";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
multiplier: {
|
||||||
|
description: "Volume Multiplier",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
markers: makeRange(1, 5, 1),
|
||||||
|
default: 2,
|
||||||
|
stickToMarkers: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "VolumeBooster",
|
name: "VolumeBooster",
|
||||||
authors: [Devs.Nuckyz],
|
authors: [Devs.Nuckyz],
|
||||||
description: "Allows you to set the user and stream volume above the default maximum.",
|
description: "Allows you to set the user and stream volume above the default maximum.",
|
||||||
|
settings,
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
// Change the max volume for sliders to allow for values above 200
|
// Change the max volume for sliders to allow for values above 200
|
||||||
@ -33,11 +45,10 @@ export default definePlugin({
|
|||||||
].map(find => ({
|
].map(find => ({
|
||||||
find,
|
find,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /maxValue:(?<defaultMaxVolumePredicate>\i\.\i)\?(?<higherMaxVolume>\d+?):(?<minorMaxVolume>\d+?),/,
|
match: /(?<=maxValue:\i\.\i)\?(\d+?):(\d+?)(?=,)/,
|
||||||
replace: ""
|
replace: (_, higherMaxVolume, minorMaxVolume) => ""
|
||||||
+ "maxValue:$<defaultMaxVolumePredicate>"
|
+ `?${higherMaxVolume}*$self.settings.store.multiplier`
|
||||||
+ "?$<higherMaxVolume>*Vencord.Settings.plugins.VolumeBooster.multiplier"
|
+ `:${minorMaxVolume}*$self.settings.store.multiplier`
|
||||||
+ ":$<minorMaxVolume>*Vencord.Settings.plugins.VolumeBooster.multiplier,"
|
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
// Prevent Audio Context Settings sync from trying to sync with values above 200, changing them to 200 before we send to Discord
|
// Prevent Audio Context Settings sync from trying to sync with values above 200, changing them to 200 before we send to Discord
|
||||||
@ -45,16 +56,16 @@ export default definePlugin({
|
|||||||
find: "AudioContextSettingsMigrated",
|
find: "AudioContextSettingsMigrated",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<restOfFunction>updateAsync\("audioContextSettings".{1,50})(?<volumeChangeExpression>return (?<volumeOptions>\i)\.volume=(?<newVolume>\i))/,
|
match: /(?<=updateAsync\("audioContextSettings".{0,50})(?=return (\i)\.volume=(\i))/,
|
||||||
replace: "$<restOfFunction>if($<newVolume>>200)return $<volumeOptions>.volume=200;$<volumeChangeExpression>"
|
replace: (_, volumeOptions, newVolume) => `if(${newVolume}>200)return ${volumeOptions}.volume=200;`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<restOfFunction>Object\.entries\(\i\.localMutes\).+?)volume:(?<volumeExpression>.+?),/,
|
match: /(?<=Object\.entries\(\i\.localMutes\).+?volume:).+?(?=,)/,
|
||||||
replace: "$<restOfFunction>volume:$<volumeExpression>>200?200:$<volumeExpression>,"
|
replace: "$&>200?200:$&"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<restOfFunction>Object\.entries\(\i\.localVolumes\).+?)volume:(?<volumeExpression>.+?)}\)/,
|
match: /(?<=Object\.entries\(\i\.localVolumes\).+?volume:).+?(?=})/,
|
||||||
replace: "$<restOfFunction>volume:$<volumeExpression>>200?200:$<volumeExpression>})"
|
replace: "$&>200?200:$&"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -63,24 +74,13 @@ export default definePlugin({
|
|||||||
find: '.displayName="MediaEngineStore"',
|
find: '.displayName="MediaEngineStore"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<restOfFunction>\.settings\.audioContextSettings.+?)(?<localVolume>\i\[\i\])=(?<syncVolume>\i\.volume)(?<secondRestOfFunction>.+?)setLocalVolume\((?<id>.+?),.+?\)/,
|
match: /(?<=\.settings\.audioContextSettings.+?)(\i\[\i\])=(\i\.volume)(.+?setLocalVolume\(\i,).+?\)/,
|
||||||
replace: ""
|
replace: (_, localVolume, syncVolume, rest) => ""
|
||||||
+ "$<restOfFunction>"
|
+ `(${localVolume}>200?void 0:${localVolume}=${syncVolume})`
|
||||||
+ "($<localVolume>>200?undefined:$<localVolume>=$<syncVolume>)"
|
+ rest
|
||||||
+ "$<secondRestOfFunction>"
|
+ `${localVolume}??${syncVolume})`
|
||||||
+ "setLocalVolume($<id>,$<localVolume>??$<syncVolume>)"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
options: {
|
|
||||||
multiplier: {
|
|
||||||
description: "Volume Multiplier",
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
markers: makeRange(1, 5, 1),
|
|
||||||
default: 2,
|
|
||||||
stickToMarkers: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
@ -23,7 +23,7 @@ export default definePlugin({
|
|||||||
name: "WebContextMenus",
|
name: "WebContextMenus",
|
||||||
description: "Re-adds some of context menu items missing on the web version of Discord, namely Copy/Open Link",
|
description: "Re-adds some of context menu items missing on the web version of Discord, namely Copy/Open Link",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
target: "WEB",
|
enabledByDefault: true,
|
||||||
|
|
||||||
patches: [{
|
patches: [{
|
||||||
// There is literally no reason for Discord to make this Desktop only.
|
// There is literally no reason for Discord to make this Desktop only.
|
@ -42,9 +42,9 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: ".Types.ORIGINAL_POSTER",
|
find: ".Types.ORIGINAL_POSTER",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /return null==(.)\?null:\(0,.{1,3}\.jsxs?\)\((.{1,3})\.Z/,
|
match: /return null==(.)\?null:\(0,.{1,3}\.jsxs?\)\((.{1,3}\.\i)/,
|
||||||
replace: (orig, type, BotTag) =>
|
replace: (orig, type, BotTag) =>
|
||||||
`if(arguments[0].message.webhookId&&arguments[0].user.isNonUserBot()){${type}=${BotTag}.Z.Types.WEBHOOK}${orig}`,
|
`if(arguments[0].message.webhookId&&arguments[0].user.isNonUserBot()){${type}=${BotTag}.Types.WEBHOOK}${orig}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -93,8 +93,8 @@ export default definePlugin({
|
|||||||
patches: [{
|
patches: [{
|
||||||
find: ",reactionRef:",
|
find: ",reactionRef:",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /((.)=(.{1,3})\.hideCount)(,.+?reactionCount.+?\}\))/,
|
match: /(?<=(\i)=(\i)\.hideCount,)(.+?reactionCount.+?\}\))/,
|
||||||
replace: "$1,whoReactedProps=$3$4,$2?null:$self.renderUsers(whoReactedProps)"
|
replace: (_, hideCount, props, rest) => `whoReactedProps=${props},${rest},${hideCount}?null:$self.renderUsers(whoReactedProps)`
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import gitRemote from "~git-remote";
|
|||||||
export const WEBPACK_CHUNK = "webpackChunkdiscord_app";
|
export const WEBPACK_CHUNK = "webpackChunkdiscord_app";
|
||||||
export const REACT_GLOBAL = "Vencord.Webpack.Common.React";
|
export const REACT_GLOBAL = "Vencord.Webpack.Common.React";
|
||||||
export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`;
|
export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`;
|
||||||
|
export const SUPPORT_CHANNEL_ID = "1026515880080842772";
|
||||||
|
|
||||||
// Add yourself here if you made a plugin
|
// Add yourself here if you made a plugin
|
||||||
export const Devs = /* #__PURE__*/ Object.freeze({
|
export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
@ -200,5 +201,9 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||||||
lewisakura: {
|
lewisakura: {
|
||||||
name: "lewisakura",
|
name: "lewisakura",
|
||||||
id: 96269247411400704n
|
id: 96269247411400704n
|
||||||
|
},
|
||||||
|
cloudburst: {
|
||||||
|
name: "cloudburst",
|
||||||
|
id: 892128204150685769n
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Vencord, a modification for Discord's desktop app
|
* Vencord, a modification for Discord's desktop app
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -16,11 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export function isTruthy<T>(item: T): item is Exclude<T, 0 | "" | false | null | undefined> {
|
||||||
export interface Badge {
|
return Boolean(item);
|
||||||
badge_name: string;
|
}
|
||||||
badge_description: string;
|
|
||||||
badge_icon: string;
|
export function isNonNullish<T>(item: T): item is Exclude<T, null | undefined> {
|
||||||
redirect_url: string;
|
return item != null;
|
||||||
badge_type: number;
|
|
||||||
}
|
}
|
@ -141,8 +141,8 @@ export function humanFriendlyJoin(elements: any[], mapper: (e: any) => string =
|
|||||||
* Calls .join(" ") on the arguments
|
* Calls .join(" ") on the arguments
|
||||||
* classes("one", "two") => "one two"
|
* classes("one", "two") => "one two"
|
||||||
*/
|
*/
|
||||||
export function classes(...classes: string[]) {
|
export function classes(...classes: Array<string | null | undefined>) {
|
||||||
return classes.filter(c => typeof c === "string").join(" ");
|
return classes.filter(Boolean).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,6 +117,7 @@ const ModalAPI = mapMangledModuleLazy("onCloseRequest:null!=", {
|
|||||||
openModal: filters.byCode("onCloseRequest:null!="),
|
openModal: filters.byCode("onCloseRequest:null!="),
|
||||||
closeModal: filters.byCode("onCloseCallback&&"),
|
closeModal: filters.byCode("onCloseCallback&&"),
|
||||||
openModalLazy: m => m?.length === 1 && filters.byCode(".apply(this,arguments)")(m),
|
openModalLazy: m => m?.length === 1 && filters.byCode(".apply(this,arguments)")(m),
|
||||||
|
closeAllModals: filters.byCode(".value.key,")
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -142,3 +143,10 @@ export function openModal(render: RenderFunction, options?: ModalOptions, contex
|
|||||||
export function closeModal(modalKey: string, contextKey?: string): void {
|
export function closeModal(modalKey: string, contextKey?: string): void {
|
||||||
return ModalAPI.closeModal(modalKey, contextKey);
|
return ModalAPI.closeModal(modalKey, contextKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close all open modals
|
||||||
|
*/
|
||||||
|
export function closeAllModals(): void {
|
||||||
|
return ModalAPI.closeAllModals();
|
||||||
|
}
|
||||||
|
@ -27,9 +27,13 @@ export function canonicalizeMatch(match: RegExp | string) {
|
|||||||
return new RegExp(canonSource, match.flags);
|
return new RegExp(canonSource, match.flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string) {
|
export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string): string | ReplaceFn {
|
||||||
if (typeof replace === "function") return replace;
|
const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`;
|
||||||
return replace.replaceAll("$self", `Vencord.Plugins.plugins.${pluginName}`);
|
|
||||||
|
if (typeof replace !== "function")
|
||||||
|
return replace.replaceAll("$self", self);
|
||||||
|
|
||||||
|
return (...args) => replace(...args).replaceAll("$self", self);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>, canonicalize: (value: T) => T) {
|
export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>, canonicalize: (value: T) => T) {
|
||||||
|
@ -76,9 +76,9 @@ export interface PluginDef {
|
|||||||
*/
|
*/
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
/**
|
/**
|
||||||
* Set this if your plugin only works on Browser or Desktop, not both
|
* Whether this plugin should be enabled by default, but can be disabled
|
||||||
*/
|
*/
|
||||||
target?: "WEB" | "DESKTOP" | "BOTH";
|
enabledByDefault?: boolean;
|
||||||
/**
|
/**
|
||||||
* Optionally provide settings that the user can configure in the Plugins tab of settings.
|
* Optionally provide settings that the user can configure in the Plugins tab of settings.
|
||||||
* @deprecated Use `settings` instead
|
* @deprecated Use `settings` instead
|
||||||
@ -229,9 +229,12 @@ type PluginSettingType<O extends PluginSettingDef> = O extends PluginSettingStri
|
|||||||
O extends PluginSettingSliderDef ? number :
|
O extends PluginSettingSliderDef ? number :
|
||||||
O extends PluginSettingComponentDef ? any :
|
O extends PluginSettingComponentDef ? any :
|
||||||
never;
|
never;
|
||||||
|
type PluginSettingDefaultType<O extends PluginSettingDef> = O extends PluginSettingSelectDef ? (
|
||||||
|
O["options"] extends { default?: boolean; }[] ? O["options"][number]["value"] : undefined
|
||||||
|
) : O extends { default: infer T; } ? T : undefined;
|
||||||
|
|
||||||
type SettingsStore<D extends SettingsDefinition> = {
|
type SettingsStore<D extends SettingsDefinition> = {
|
||||||
[K in keyof D]: PluginSettingType<D[K]>;
|
[K in keyof D]: PluginSettingType<D[K]> | PluginSettingDefaultType<D[K]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** An instance of defined plugin settings */
|
/** An instance of defined plugin settings */
|
||||||
|
@ -77,3 +77,25 @@ export async function rebuild() {
|
|||||||
return oldHashes["patcher.js"] !== newHashes["patcher.js"] ||
|
return oldHashes["patcher.js"] !== newHashes["patcher.js"] ||
|
||||||
oldHashes["preload.js"] !== newHashes["preload.js"];
|
oldHashes["preload.js"] !== newHashes["preload.js"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function maybePromptToUpdate(confirmMessage: string, checkForDev = false) {
|
||||||
|
if (IS_WEB) return;
|
||||||
|
if (checkForDev && IS_DEV) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isOutdated = await checkForUpdates();
|
||||||
|
if (isOutdated) {
|
||||||
|
const wantsUpdate = confirm(confirmMessage);
|
||||||
|
if (wantsUpdate && isNewer) return alert("Your local copy has more recent commits. Please stash or reset them.");
|
||||||
|
if (wantsUpdate) {
|
||||||
|
await update();
|
||||||
|
const needFullRestart = await rebuild();
|
||||||
|
if (needFullRestart) DiscordNative.app.relaunch();
|
||||||
|
else location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
UpdateLogger.error(err);
|
||||||
|
alert("That also failed :( Try updating or re-installing with the installer!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -35,7 +35,7 @@ export const Forms = {
|
|||||||
export const Card = waitForComponent<t.Card>("Card", m => m.Types?.PRIMARY && m.defaultProps);
|
export const Card = waitForComponent<t.Card>("Card", m => m.Types?.PRIMARY && m.defaultProps);
|
||||||
export const Button = waitForComponent<t.Button>("Button", ["Hovers", "Looks", "Sizes"]);
|
export const Button = waitForComponent<t.Button>("Button", ["Hovers", "Looks", "Sizes"]);
|
||||||
export const Switch = waitForComponent<t.Switch>("Switch", filters.byCode("tooltipNote", "ringTarget"));
|
export const Switch = waitForComponent<t.Switch>("Switch", filters.byCode("tooltipNote", "ringTarget"));
|
||||||
export const Tooltip = waitForComponent<t.Tooltip>("Tooltip", ["Positions", "Colors"]);
|
export const Tooltip = waitForComponent<t.Tooltip>("Tooltip", filters.byCode("shouldShowTooltip:!1", "clickableOnMobile||"));
|
||||||
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format"));
|
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format"));
|
||||||
export const TextInput = waitForComponent<t.TextInput>("TextInput", ["defaultProps", "Sizes", "contextType"]);
|
export const TextInput = waitForComponent<t.TextInput>("TextInput", ["defaultProps", "Sizes", "contextType"]);
|
||||||
export const TextArea = waitForComponent<t.TextArea>("TextArea", filters.byCode("handleSetRef", "textArea"));
|
export const TextArea = waitForComponent<t.TextArea>("TextArea", filters.byCode("handleSetRef", "textArea"));
|
||||||
@ -45,12 +45,12 @@ export const Text = waitForComponent<t.Text>("Text", m => {
|
|||||||
return (s.length < 1500 && s.includes("data-text-variant") && s.includes("always-white"));
|
return (s.length < 1500 && s.includes("data-text-variant") && s.includes("always-white"));
|
||||||
});
|
});
|
||||||
export const Select = waitForComponent<t.Select>("Select", filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
|
export const Select = waitForComponent<t.Select>("Select", filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
|
||||||
|
const searchableSelectFilter = filters.byCode("autoFocus", ".Messages.SELECT");
|
||||||
|
export const SearchableSelect = waitForComponent<t.SearchableSelect>("SearchableSelect", m =>
|
||||||
|
m.render && searchableSelectFilter(m.render)
|
||||||
|
);
|
||||||
export const Slider = waitForComponent<t.Slider>("Slider", filters.byCode("closestMarkerIndex", "stickToMarkers"));
|
export const Slider = waitForComponent<t.Slider>("Slider", filters.byCode("closestMarkerIndex", "stickToMarkers"));
|
||||||
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
|
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
|
||||||
|
|
||||||
export const ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent") as Record<string, string>;
|
export const ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent") as Record<string, string>;
|
||||||
/**
|
|
||||||
* @deprecated Use @utils/margins instead
|
|
||||||
*/
|
|
||||||
export const Margins: t.Margins = findByPropsLazy("marginTop20");
|
|
||||||
export const ButtonLooks: t.ButtonLooks = findByPropsLazy("BLANK", "FILLED", "INVERTED");
|
export const ButtonLooks: t.ButtonLooks = findByPropsLazy("BLANK", "FILLED", "INVERTED");
|
||||||
|
51
src/webpack/common/types/components.d.ts
vendored
51
src/webpack/common/types/components.d.ts
vendored
@ -90,16 +90,17 @@ export type Tooltip = ComponentType<{
|
|||||||
|
|
||||||
/** Tooltip.Colors.BLACK */
|
/** Tooltip.Colors.BLACK */
|
||||||
color?: string;
|
color?: string;
|
||||||
/** Tooltip.Positions.TOP */
|
/** TooltipPositions.TOP */
|
||||||
position?: string;
|
position?: string;
|
||||||
|
|
||||||
tooltipClassName?: string;
|
tooltipClassName?: string;
|
||||||
tooltipContentClassName?: string;
|
tooltipContentClassName?: string;
|
||||||
}> & {
|
}> & {
|
||||||
Positions: Record<"BOTTOM" | "CENTER" | "LEFT" | "RIGHT" | "TOP" | "WINDOW_CENTER", string>;
|
|
||||||
Colors: Record<"BLACK" | "BRAND" | "CUSTOM" | "GREEN" | "GREY" | "PRIMARY" | "RED" | "YELLOW", string>;
|
Colors: Record<"BLACK" | "BRAND" | "CUSTOM" | "GREEN" | "GREY" | "PRIMARY" | "RED" | "YELLOW", string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TooltipPositions = Record<"BOTTOM" | "CENTER" | "LEFT" | "RIGHT" | "TOP" | "WINDOW_CENTER", string>;
|
||||||
|
|
||||||
export type Card = ComponentType<PropsWithChildren<HTMLProps<HTMLDivElement> & {
|
export type Card = ComponentType<PropsWithChildren<HTMLProps<HTMLDivElement> & {
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
outline?: boolean;
|
outline?: boolean;
|
||||||
@ -234,6 +235,49 @@ export type Select = ComponentType<PropsWithChildren<{
|
|||||||
"aria-labelledby"?: boolean;
|
"aria-labelledby"?: boolean;
|
||||||
}>>;
|
}>>;
|
||||||
|
|
||||||
|
export type SearchableSelect = ComponentType<PropsWithChildren<{
|
||||||
|
placeholder?: string;
|
||||||
|
options: ReadonlyArray<SelectOption>; // TODO
|
||||||
|
value?: SelectOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - 0 ~ Filled
|
||||||
|
* - 1 ~ Custom
|
||||||
|
*/
|
||||||
|
look?: 0 | 1;
|
||||||
|
className?: string;
|
||||||
|
popoutClassName?: string;
|
||||||
|
wrapperClassName?: string;
|
||||||
|
popoutPosition?: "top" | "left" | "right" | "bottom" | "center" | "window_center";
|
||||||
|
optionClassName?: string;
|
||||||
|
|
||||||
|
autoFocus?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
clearable?: boolean;
|
||||||
|
closeOnSelect?: boolean;
|
||||||
|
clearOnSelect?: boolean;
|
||||||
|
multi?: boolean;
|
||||||
|
|
||||||
|
onChange(value: any): void;
|
||||||
|
onSearchChange?(value: string): void;
|
||||||
|
|
||||||
|
onClose?(): void;
|
||||||
|
onOpen?(): void;
|
||||||
|
onBlur?(): void;
|
||||||
|
|
||||||
|
renderOptionPrefix?(option: SelectOption): ReactNode;
|
||||||
|
renderOptionSuffix?(option: SelectOption): ReactNode;
|
||||||
|
|
||||||
|
filter?(option: SelectOption[], query: string): SelectOption[];
|
||||||
|
|
||||||
|
centerCaret?: boolean;
|
||||||
|
debounceTime?: number;
|
||||||
|
maxVisibleItems?: number;
|
||||||
|
popoutWidth?: number;
|
||||||
|
|
||||||
|
"aria-labelledby"?: boolean;
|
||||||
|
}>>;
|
||||||
|
|
||||||
export type Slider = ComponentType<PropsWithChildren<{
|
export type Slider = ComponentType<PropsWithChildren<{
|
||||||
initialValue: number;
|
initialValue: number;
|
||||||
defaultValue?: number;
|
defaultValue?: number;
|
||||||
@ -278,7 +322,4 @@ export type Flex = ComponentType<PropsWithChildren<any>> & {
|
|||||||
Direction: Record<"VERTICAL" | "HORIZONTAL" | "HORIZONTAL_REVERSE", string>;
|
Direction: Record<"VERTICAL" | "HORIZONTAL" | "HORIZONTAL_REVERSE", string>;
|
||||||
Justify: Record<"START" | "END" | "CENTER" | "BETWEEN" | "AROUND", string>;
|
Justify: Record<"START" | "END" | "CENTER" | "BETWEEN" | "AROUND", string>;
|
||||||
Wrap: Record<"NO_WRAP" | "WRAP" | "WRAP_REVERSE", string>;
|
Wrap: Record<"NO_WRAP" | "WRAP" | "WRAP_REVERSE", string>;
|
||||||
|
|
||||||
Content: ComponentType<PropsWithChildren<any>>;
|
|
||||||
Sidebar: ComponentType<PropsWithChildren<any>>;
|
|
||||||
};
|
};
|
||||||
|
@ -92,9 +92,11 @@ function patchPush() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const numberId = Number(id);
|
||||||
|
|
||||||
for (const callback of listeners) {
|
for (const callback of listeners) {
|
||||||
try {
|
try {
|
||||||
callback(exports);
|
callback(exports, numberId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Error in webpack listener", err);
|
logger.error("Error in webpack listener", err);
|
||||||
}
|
}
|
||||||
@ -104,17 +106,17 @@ function patchPush() {
|
|||||||
try {
|
try {
|
||||||
if (filter(exports)) {
|
if (filter(exports)) {
|
||||||
subscriptions.delete(filter);
|
subscriptions.delete(filter);
|
||||||
callback(exports);
|
callback(exports, numberId);
|
||||||
} else if (typeof exports === "object") {
|
} else if (typeof exports === "object") {
|
||||||
if (exports.default && filter(exports.default)) {
|
if (exports.default && filter(exports.default)) {
|
||||||
subscriptions.delete(filter);
|
subscriptions.delete(filter);
|
||||||
callback(exports.default);
|
callback(exports.default, numberId);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const nested in exports) if (nested.length <= 3) {
|
for (const nested in exports) if (nested.length <= 3) {
|
||||||
if (exports[nested] && filter(exports[nested])) {
|
if (exports[nested] && filter(exports[nested])) {
|
||||||
subscriptions.delete(filter);
|
subscriptions.delete(filter);
|
||||||
callback(exports[nested]);
|
callback(exports[nested], numberId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ export const filters = {
|
|||||||
export const subscriptions = new Map<FilterFn, CallbackFn>();
|
export const subscriptions = new Map<FilterFn, CallbackFn>();
|
||||||
export const listeners = new Set<CallbackFn>();
|
export const listeners = new Set<CallbackFn>();
|
||||||
|
|
||||||
export type CallbackFn = (mod: any) => void;
|
export type CallbackFn = (mod: any, id: number) => void;
|
||||||
|
|
||||||
export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
|
export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
|
||||||
if (cache !== void 0) throw "no.";
|
if (cache !== void 0) throw "no.";
|
||||||
@ -86,18 +86,23 @@ export const find = traceFunction("find", function find(filter: FilterFn, getDef
|
|||||||
const mod = cache[key];
|
const mod = cache[key];
|
||||||
if (!mod?.exports) continue;
|
if (!mod?.exports) continue;
|
||||||
|
|
||||||
if (filter(mod.exports))
|
if (filter(mod.exports)) {
|
||||||
return mod.exports;
|
return isWaitFor ? [mod.exports, Number(key)] : mod.exports;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof mod.exports !== "object") continue;
|
if (typeof mod.exports !== "object") continue;
|
||||||
|
|
||||||
if (mod.exports.default && filter(mod.exports.default))
|
if (mod.exports.default && filter(mod.exports.default)) {
|
||||||
return getDefault ? mod.exports.default : mod.exports;
|
const found = getDefault ? mod.exports.default : mod.exports;
|
||||||
|
return isWaitFor ? [found, Number(key)] : found;
|
||||||
|
}
|
||||||
|
|
||||||
// the length check makes search about 20% faster
|
// the length check makes search about 20% faster
|
||||||
for (const nestedMod in mod.exports) if (nestedMod.length <= 3) {
|
for (const nestedMod in mod.exports) if (nestedMod.length <= 3) {
|
||||||
const nested = mod.exports[nestedMod];
|
const nested = mod.exports[nestedMod];
|
||||||
if (nested && filter(nested)) return nested;
|
if (nested && filter(nested)) {
|
||||||
|
return isWaitFor ? [nested, Number(key)] : nested;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +117,7 @@ export const find = traceFunction("find", function find(filter: FilterFn, getDef
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return isWaitFor ? [null, null] : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -307,13 +312,6 @@ export function findByPropsLazy(...props: string[]) {
|
|||||||
return findLazy(filters.byProps(...props));
|
return findLazy(filters.byProps(...props));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all modules that have the specified properties
|
|
||||||
*/
|
|
||||||
export function findAllByProps(...props: string[]) {
|
|
||||||
return findAll(filters.byProps(...props));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a function by its code
|
* Find a function by its code
|
||||||
*/
|
*/
|
||||||
@ -354,8 +352,8 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback
|
|||||||
else if (typeof filter !== "function")
|
else if (typeof filter !== "function")
|
||||||
throw new Error("filter must be a string, string[] or function, got " + typeof filter);
|
throw new Error("filter must be a string, string[] or function, got " + typeof filter);
|
||||||
|
|
||||||
const existing = find(filter!, true, true);
|
const [existing, id] = find(filter!, true, true);
|
||||||
if (existing) return void callback(existing);
|
if (existing) return void callback(existing, id);
|
||||||
|
|
||||||
subscriptions.set(filter, callback);
|
subscriptions.set(filter, callback);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user