Compare commits

...

24 Commits

Author SHA1 Message Date
Vendicated
ed873ef9de Bump to v1.1.1 2023-03-15 18:01:04 +01:00
LordElias
d8a553feb0 improve MessageLogger deleted image hover animation (#603) 2023-03-14 18:48:36 +01:00
Ven
4717612090 :shipit: 2023-03-12 16:37:41 +01:00
Vendicated
5d1283bd85 Add Web/Desktop specific plugin capabilities; misc fixes 2023-03-11 14:18:32 +01:00
Ven
3b945b87b8 Delete src/plugins/reviewDB directory
Api owner refusing to properly moderate hate speech and related illegal / ToS infringing content
2023-03-11 12:26:54 +01:00
Vendicated
19c762f9c1 DevCompanion: Fix Deps 2023-03-11 00:28:27 +01:00
Vendicated
990adf7527 Fix casing in filename 2023-03-11 00:27:02 +01:00
Vendicated
983414d024 Add DevCompanion plugin (https://github.com/Vencord/Companion) 2023-03-11 00:25:49 +01:00
Vendicated
d5c05d857f Add DevOnly plugin capability 2023-03-11 00:25:32 +01:00
Nuckyz
bff6788546 feat(plugins): SilentMessageToggle (#586)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-09 01:19:28 +01:00
Nuckyz
253183a16a Fix Emote Cloner and improve ReverseImageSearch (#489) 2023-03-08 04:01:24 -03:00
Nuckyz
0fb3901a18 Fix Context Menu API (#583) 2023-03-08 06:01:15 +00:00
Nuckyz
1b199ec5d8 feat: Context Menu API (#496) 2023-03-08 01:59:50 -03:00
Nuckyz
40395d562a Improvements for patches and misc stuff (#582) 2023-03-08 00:11:59 -03:00
Nuckyz
7322c3af04 Fix Crash Loops and prevent metrics (#580) 2023-03-06 22:54:01 +01:00
Nuckyz
36c27f1111 VCDoubleClick: Fix applying to non voice channels (#572) 2023-03-06 02:39:53 +01:00
Nuckyz
95db6c32a3 Fix Ignore Activities button on platforms different than Windows (#528)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-06 00:12:52 +01:00
Nuckyz
bed5e98bb0 Misc fixes and improvements (#555)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-05 22:49:59 +01:00
Nico
a5392e5c53 fix(silentTyping): fix chatbar icon patch (#570) 2023-03-05 22:30:37 +01:00
Sammy
abbd298b31 Fix(InvisibleChat) Fix chatbar icon patch (closes #560) (#566)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-05 22:05:46 +01:00
Nuckyz
e219aaa062 Notifications: Permanent option and close button (#563)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-04 18:49:15 +01:00
Vendicated
cab72e1be6 Strongly type useSettings (supersedes #559) 2023-03-04 18:41:32 +01:00
Berlin
92372bde1d Update 1_INSTALLING.md (#562) 2023-03-03 23:55:21 +00:00
megumin
6747276a87 Add admin warnings to INSTALLING.md (#561) 2023-03-03 23:07:48 +00:00
59 changed files with 1242 additions and 1146 deletions

View File

@ -41,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.

View File

@ -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

View File

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.1.0", "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",

View File

@ -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`;

141
src/api/ContextMenu.ts Normal file
View 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);
}
}
}

View File

@ -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)" }}

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -167,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(() => {
@ -229,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 ?? {},
@ -237,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;
};

View File

@ -93,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;

3
src/globals.d.ts vendored
View File

@ -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;

View 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});`
}
}
]
});

View File

@ -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);`
} }
] ]
} }

View File

@ -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

View File

@ -32,14 +32,14 @@ export default definePlugin({
authors: [Devs.Ven], authors: [Devs.Ven],
getShortcuts() { getShortcuts() {
function newFindWrapper(filterFactory: (props: any) => Webpack.FilterFn) { function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) {
const cache = new Map<string, any>(); const cache = new Map<string, unknown>();
return function (filterProps: any) { return function (...filterProps: unknown[]) {
const cacheKey = String(filterProps); const cacheKey = String(filterProps);
if (cache.has(cacheKey)) return cache.get(cacheKey); if (cache.has(cacheKey)) return cache.get(cacheKey);
const matches = findAll(filterFactory(filterProps)); const matches = findAll(filterFactory(...filterProps));
const result = (() => { const result = (() => {
switch (matches.length) { switch (matches.length) {

View File

@ -41,6 +41,8 @@ const settings = definePluginSettings({
} }
}); });
let crashCount: number = 0;
export default definePlugin({ export default definePlugin({
name: "CrashHandler", name: "CrashHandler",
description: "Utility plugin for handling and possibly recovering from Crashes without a restart", description: "Utility plugin for handling and possibly recovering from Crashes without a restart",
@ -62,15 +64,29 @@ export default definePlugin({
{ {
find: 'dispatch({type:"MODAL_POP_ALL"})', find: 'dispatch({type:"MODAL_POP_ALL"})',
replacement: { replacement: {
match: /(?<=(?<popAll>\i)=function\(\){\(0,\i\.\i\)\(\);\i\.\i\.dispatch\({type:"MODAL_POP_ALL"}\).+};)/, match: /"MODAL_POP_ALL".+?};(?<=(\i)=function.+?)/,
replace: "$self.popAllModals=$<popAll>;" replace: (m, popAll) => `${m}$self.popAllModals=${popAll};`
} }
} }
], ],
handleCrash(_this: ReactElement & { forceUpdate: () => void; }) { 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 { try {
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 (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) { if (settings.store.attemptToPreventCrashes) {
this.handlePreventCrash(_this); this.handlePreventCrash(_this);
@ -80,6 +96,7 @@ export default definePlugin({
return false; return false;
} catch (err) { } catch (err) {
CrashHandlerLogger.error("Failed to handle crash", err); CrashHandlerLogger.error("Failed to handle crash", err);
return false;
} }
}, },

View 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;
}
});

View File

@ -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;"
}, }
}, }
], ]
}); });

View File

@ -16,12 +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 { 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 { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { makeLazy } from "@utils/misc";
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";
@ -176,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);
}
}); });

View File

@ -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");

View File

@ -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;"
}; };
}) })
}, },

View File

@ -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;
}, }
}); });

View File

@ -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{}",
} }
}, },
], ],

View File

@ -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 {

View File

@ -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();
} }

View File

@ -25,7 +25,6 @@ export default definePlugin({
name: "NoRPC", 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")',

View File

@ -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})];`
})) }))
}, }
], ]
}); });

View File

@ -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",

View File

@ -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;"
}
]
} }
] ]
}); });

View File

@ -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"
} }
} }
], ],

View File

@ -32,8 +32,8 @@ export default definePlugin({
{ {
find: ".removeObscurity=function", find: ".removeObscurity=function",
replacement: { replacement: {
match: /\.removeObscurity=function\((\i)\){/, match: /(?<=\.removeObscurity=function\((\i)\){)/,
replace: ".removeObscurity=function($1){$self.reveal($1);" replace: (_, event) => `$self.reveal(${event});`
} }
} }
], ],

View File

@ -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");
} }
}); });

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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>
);
};
});

View File

@ -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>
);
}

View File

@ -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>
);
};
});

View File

@ -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>
);
}

View File

@ -1,26 +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/>.
*/
export interface Badge {
badge_name: string;
badge_description: string;
badge_icon: string;
redirect_url: string;
badge_type: number;
}

View File

@ -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[];
}

View File

@ -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>
)
});

View File

@ -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",

View File

@ -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;

View 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 }),
});

View File

@ -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{}",
} }
}, },
], ],

View File

@ -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: ""
}
]
} }
} ]
}); });

View File

@ -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)`
} }
} }
], ],

View File

@ -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"
} }
}, },

View File

@ -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;
@ -50,12 +50,19 @@ export default definePlugin({
// channel mentions // channel mentions
find: ".shouldCloseDefaultModals", 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) {

View File

@ -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,
}
}
}); });

View File

@ -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.

View File

@ -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}`,
}, },
}, },
], ],

View File

@ -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)`
} }
}], }],

View File

@ -79,10 +79,6 @@ export interface PluginDef {
* Whether this plugin should be enabled by default, but can be disabled * Whether this plugin should be enabled by default, but can be disabled
*/ */
enabledByDefault?: boolean; enabledByDefault?: boolean;
/**
* Set this if your plugin only works on Browser or Desktop, not both
*/
target?: "WEB" | "DESKTOP" | "BOTH";
/** /**
* 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

View File

@ -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);
} }
} }
} }

View File

@ -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;
}); });
/** /**
@ -347,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);
} }