Merge branch 'dev' into main

This commit is contained in:
byeoon 2024-07-04 01:12:59 -04:00 committed by GitHub
commit ae6290a605
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 1571 additions and 833 deletions

View file

@ -1,6 +1,12 @@
{ {
"extends": "stylelint-config-standard", "extends": "stylelint-config-standard",
"rules": { "rules": {
"indentation": 4 "indentation": 4,
"selector-class-pattern": [
"^[a-z][a-zA-Z0-9]*(-[a-z0-9][a-zA-Z0-9]*)*$",
{
"message": "Expected class selector to be kebab-case with camelCase segments"
}
]
} }
} }

View file

@ -16,5 +16,6 @@ DON'T
Repetitive violations of these guidelines might get your access to the repository restricted. Repetitive violations of these guidelines might get your access to the repository restricted.
If you feel like a user is violating these guidelines or feel treated unfairly, please refrain from vigilantism
If you feel like a user is violating these guidelines or feel treated unfairly, please refrain from publicly challenging them and instead contact a Moderator on our Discord server or send an email to vendicated+conduct@riseup.net! and instead report the issue to a moderator! The best way is joining our [official Discord community](https://vencord.dev/discord)
and opening a modmail ticket.

View file

@ -1,82 +1,55 @@
# Contribution Guide # Contributing to Vencord
First of all, thank you for contributing! :3 Vencord is a community project and welcomes any kind of contribution from anyone!
To ensure your contribution is robust, please follow the below guide! We have development documentation for new contributors, which can be found at <https://docs.vencord.dev>.
For a friendly introduction to plugins, see [Megu's Plugin Guide!](docs/2_PLUGINS.md) All contributions should be made in accordance with our [Code of Conduct](./CODE_OF_CONDUCT.md).
## Style Guide ## How to contribute
- This project has a very minimal .editorconfig. Make sure your editor supports this! Contributions can be sent via pull requests. If you're new to Git, check [this guide](https://opensource.com/article/19/7/create-pull-request-github).
If you are using VSCode, it should automatically recommend you the extension; If not,
please install the Editorconfig extension
- Try to follow the formatting in the rest of the project and stay consistent
- Follow the file naming convention. File names should usually be camelCase, unless they export a Class
or React Component, in which case they should be PascalCase
## Contributing a Plugin Pull requests can be made either to the `main` or the `dev` branch. However, unless you're an advanced user, I recommend sticking to `main`. This is because the dev branch might contain unstable changes and be force pushed frequently, which could cause conflicts in your pull request.
Because plugins modify code directly, incompatibilities are a problem. ## Write a plugin
Thus, 3rd party plugins are not supported, instead all plugins are part of Vencord itself. Writing a plugin is the primary way to contribute.
This way we can ensure compatibility and high quality patches.
Follow the below guide to make your first plugin! Before starting your plugin:
- Check existing pull requests to see if someone is already working on a similar plugin
- Check our [plugin requests tracker](https://github.com/Vencord/plugin-requests/issues) to see if there is an existing request, or if the same idea has been rejected
- If there isn't an existing request, [open one](https://github.com/Vencord/plugin-requests/issues/new?assignees=&labels=&projects=&template=request.yml) yourself
and include that you'd like to work on this yourself. Then wait for feedback to see if the idea even has any chance of being accepted. Or maybe others have some ideas to improve it!
- Familarise yourself with our plugin rules below to ensure your plugin is not banned
### Finding the right module to patch ### Plugin Rules
If the thing you want to patch is an action performed when interacting with a part of the UI, use React DevTools. - No simple slash command plugins like `/cat`. Instead, make a [user installable Discord bot](https://discord.com/developers/docs/change-log#userinstallable-apps-preview)
They come preinstalled and can be found as the "Components" tab in DevTools. - No simple text replace plugins like Let me Google that for you. The TextReplace plugin can do this
Use the Selector (top left) to select the UI Element. Now you can see all callbacks, props or jump to the source - No raw DOM manipulation. Use proper patches and React
directly. - No FakeDeafen or FakeMute
- No StereoMic
- No plugins that simply hide or redesign ui elements. This can be done with CSS
- No selfbots or API spam (animated status, message pruner, auto reply, nitro snipers, etc)
- No untrusted third party APIs. Popular services like Google or GitHub are fine, but absolutely no self hosted ones
- No plugins that require the user to enter their own API key
- Do not introduce new dependencies unless absolutely necessary and warranted
If it is anything else, or you're too lazy to use React DevTools, hit `CTRL + Shift + F` while in DevTools and ## Improve Vencord itself
enter a search term, for example "getUser" to search all source files.
Look at the results until you find something promising. Set a breakpoint and trigger the execution of that part of Code to inspect arguments, locals, etc...
### Writing a robust patch If you have any ideas on how to improve Vencord itself, or want to propose a new plugin API, feel free to open a feature request so we can discuss.
##### "find" Or if you notice any bugs or typos, feel free to fix them!
First you need to find a good `find` value. This should be a string that is unique to your module. ## Contribute to our Documentation
If you want to patch the `getUser` function, usually a good first try is `getUser:` or `function getUser()`,
depending on how the module is structured. Again, make sure this string is unique to your module and is not
found in any other module. To verify this, search for it in all bundles (CTRL + Shift + F)
##### "match" The source code of our documentation is available at <https://github.com/Vencord/Docs>
This is the regex that will operate on the module found with "find". Just like in find, you should make sure If you see anything outdated, incorrect or lacking, please fix it!
this only matches exactly the part you want to patch and no other parts in the file. If you think a new page should be added, feel free to suggest it via an issue and we can discuss.
The easiest way to write and test your regex is the following: ## Help out users in our Discord community
- Get the ID of the module you want to patch. To do this, go to it in the sources tab and scroll up until you We have an open support channel in our [Discord community](https://vencord.dev/discord).
see something like `447887: (e,t,n)=>{` (Obviously the number will differ). Helping out users there is always appreciated! The more, the merrier.
- Now paste the following into the console: `Vencord.Webpack.wreq.m[447887].toString()` (Changing the number to your ID)
- Now either test regexes on this string in the console or use a tool like https://regex101.com
Also pay attention to the following:
- Never hardcode variable or parameter names or any other minified names. They will change in the future. The only Exception to this rule
are the react props parameter which seems to always be `e`, but even then only rely on this if it is necessary.
Instead, use one of the following approaches where applicable:
- Match 1 or 2 of any character: `.{1,2}`, for example to match the variable name in `var a=b`, `var (.{1,2})=`
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
`var .{1,2}=([^;]+);`
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
- Additionally, as you might have noticed, all of the above approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
#### "replace"
This is the replacement for the match. This is the second argument to [String.replace](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace), so refer to those docs for info.
Never hardcode minified variable or parameter names here. Instead, use capture groups in your regex to capture the variable names
and use those in your replacement
Make sure your replacement does not introduce any whitespace. While this might seem weird, random whitespace may mess up other patches.
This includes spaces, tabs and especially newlines
---
And that's it! Now open a Pull Request with your Plugin

View file

@ -2,23 +2,22 @@ if (typeof browser === "undefined") {
var browser = chrome; var browser = chrome;
} }
const script = document.createElement("script");
script.src = browser.runtime.getURL("dist/Vencord.js");
script.id = "vencord-script";
Object.assign(script.dataset, {
extensionBaseUrl: browser.runtime.getURL(""),
version: browser.runtime.getManifest().version
});
const style = document.createElement("link"); const style = document.createElement("link");
style.type = "text/css"; style.type = "text/css";
style.rel = "stylesheet"; style.rel = "stylesheet";
style.href = browser.runtime.getURL("dist/Vencord.css"); style.href = browser.runtime.getURL("dist/Vencord.css");
document.documentElement.append(script);
document.addEventListener( document.addEventListener(
"DOMContentLoaded", "DOMContentLoaded",
() => document.documentElement.append(style), () => {
document.documentElement.append(style);
window.postMessage({
type: "vencord:meta",
meta: {
EXTENSION_VERSION: browser.runtime.getManifest().version,
EXTENSION_BASE_URL: browser.runtime.getURL(""),
}
});
},
{ once: true } { once: true }
); );

View file

@ -1,6 +1,6 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"minimum_chrome_version": "91", "minimum_chrome_version": "111",
"name": "Vencord Web", "name": "Vencord Web",
"description": "The cutest Discord mod now in your browser", "description": "The cutest Discord mod now in your browser",
@ -22,7 +22,15 @@
"run_at": "document_start", "run_at": "document_start",
"matches": ["*://*.discord.com/*"], "matches": ["*://*.discord.com/*"],
"js": ["content.js"], "js": ["content.js"],
"all_frames": true "all_frames": true,
"world": "ISOLATED"
},
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["dist/Vencord.js"],
"all_frames": true,
"world": "MAIN"
} }
], ],

View file

@ -22,7 +22,15 @@
"run_at": "document_start", "run_at": "document_start",
"matches": ["*://*.discord.com/*"], "matches": ["*://*.discord.com/*"],
"js": ["content.js"], "js": ["content.js"],
"all_frames": true "all_frames": true,
"world": "ISOLATED"
},
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["dist/Vencord.js"],
"all_frames": true,
"world": "MAIN"
} }
], ],
@ -35,7 +43,7 @@
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "vencord-firefox@vendicated.dev", "id": "vencord-firefox@vendicated.dev",
"strict_min_version": "91.0" "strict_min_version": "128.0"
} }
} }
} }

View file

@ -1,97 +0,0 @@
> [!WARNING]
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
> No support will be provided for installing in this fashion. If you cannot figure it out, you should just stick to a regular install.
# Installation Guide
Welcome to Megu's Installation Guide! In this file, you will learn about how to download, install, and uninstall Vencord!
## Sections
- [Installation Guide](#installation-guide)
- [Sections](#sections)
- [Dependencies](#dependencies)
- [Installing Vencord](#installing-vencord)
- [Updating Vencord](#updating-vencord)
- [Uninstalling Vencord](#uninstalling-vencord)
## Dependencies
- Install Git from https://git-scm.com/download
- Install Node.JS LTS from here: https://nodejs.dev/en/
## Installing Vencord
Install `pnpm`:
> :exclamation: This next command may need to be run as admin/root depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
```shell
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:
```shell
git clone https://github.com/Vendicated/Vencord
cd Vencord
```
Install dependencies:
```shell
pnpm install --frozen-lockfile
```
Build Vencord:
```shell
pnpm build
```
Inject vencord into your client:
```shell
pnpm inject
```
Then fully close Discord from your taskbar or task manager, and restart it. Vencord should be injected - you can check this by looking for the Vencord section in Discord settings.
## Updating Vencord
If you're using Discord already, go into the `Updater` tab in settings.
Sometimes it may be necessary to manually update if the GUI updater fails.
To pull latest changes:
```shell
git pull
```
If this fails, you likely need to reset your local changes to vencord to resolve merge errors:
> :exclamation: This command will remove any local changes you've made to vencord. Make sure you back up if you made any code changes you don't want to lose!
```shell
git reset --hard
git pull
```
and then to build the changes:
```shell
pnpm build
```
Then just refresh your client
## Uninstalling Vencord
Simply run:
```shell
pnpm uninject
```

View file

@ -1,111 +0,0 @@
# Plugins Guide
Welcome to Megu's Plugin Guide! In this file, you will learn about how to write your own plugin!
You don't need to run `pnpm build` every time you make a change. Instead, use `pnpm watch` - this will auto-compile Vencord whenever you make a change. If using code patches (recommended), you will need to CTRL+R to load the changes.
## Plugin Entrypoint
> If it doesn't already exist, create a folder called `userplugins` in the `src` directory of this repo.
1. Create a folder in `src/userplugins/` with the name of your plugin. For example, `src/userplugins/epicPlugin/` - All of your plugin files will go here.
2. Create a file in that folder called `index.ts`
3. In `index.ts`, copy-paste the following template code:
```ts
import definePlugin from "@utils/types";
export default definePlugin({
name: "Epic Plugin",
description: "This plugin is absolutely epic",
authors: [
{
id: 12345n,
name: "Your Name",
},
],
patches: [],
// Delete these two below if you are only using code patches
start() {},
stop() {},
});
```
Change the name, description, and authors to your own information.
Replace `12345n` with your user ID ending in `n` (e.g., `545581357812678656n`). If you don't want to share your Discord account, use `0n` instead!
## How Plugins Work In Vencord
Vencord uses a different way of making mods than you're used to.
Instead of monkeypatching webpack, we directly modify the code before Discord loads it.
This is _significantly_ more efficient than monkeypatching webpack, and is surprisingly easy, but it may be confusing at first.
## Making your patch
For an in-depth guide into patching code, see [CONTRIBUTING.md](../CONTRIBUTING.md)
in the `index.ts` file we made earlier, you'll see a `patches` array.
> You'll see examples of how patches are used in all the existing plugins, and it'll be easier to understand by looking at those examples, so do that first, and then return here!
> For a good example of a plugin using code patches AND runtime patching, check `src/plugins/unindent.ts`, which uses code patches to run custom runtime code.
One of the patches in the `isStaff` plugin, looks like this:
```ts
{
match: /(\w+)\.isStaff=function\(\){return\s*!1};/,
replace: "$1.isStaff=function(){return true};",
},
```
The above regex matches the string in discord that will look something like:
```js
abc.isStaff = function () {
return !1;
};
```
Remember that Discord code is minified, so there won't be any newlines, and there will only be spaces where necessary. So the source code looks something like:
```
abc.isStaff=function(){return!1;}
```
You can find these snippets by opening the devtools (`ctrl+shift+i`) and pressing `ctrl+shift+f`, searching for what you're looking to modify in there, and beautifying the file to make it more readable.
In the `match` regex in the example shown above, you'll notice at the start there is a `(\w+)`.
Anything in the brackets will be accessible in the `replace` string using `$<number>`. e.g., the first pair of brackets will be `$1`, the second will be `$2`, etc.
The replacement string we used is:
```
"$1.isStaff=function(){return true;};"
```
Which, using the above example, would replace the code with:
> **Note**
> In this example, `$1` becomes `abc`
```js
abc.isStaff = function () {
return true;
};
```
The match value _can_ be a string, rather than regex, however usually regex will be better suited, as it can work with unknown values, whereas strings must be exact matches.
Once you've made your plugin, make sure you run `pnpm test` and make sure your code is nice and clean!
If you want to publish your plugin into the Vencord repo, move your plugin from `src/userplugins` into the `src/plugins` folder and open a PR!
> **Warning**
> Make sure you've read [CONTRIBUTING.md](../CONTRIBUTING.md) before opening a PR
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.9.0", "version": "1.9.3",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -13,9 +13,6 @@
}, },
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"author": "Vendicated", "author": "Vendicated",
"directories": {
"doc": "docs"
},
"scripts": { "scripts": {
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs", "build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
"buildStandalone": "pnpm build --standalone", "buildStandalone": "pnpm build --standalone",

View file

@ -136,7 +136,6 @@ async function printReport() {
body: JSON.stringify({ body: JSON.stringify({
description: "Here's the latest Vencord Report!", description: "Here's the latest Vencord Report!",
username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""), username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
avatar_url: "https://cdn.discordapp.com/avatars/1017176847865352332/c312b6b44179ae6817de7e4b09e9c6af.webp?size=512",
embeds: [ embeds: [
{ {
title: "Bad Patches", title: "Bad Patches",
@ -290,6 +289,8 @@ page.on("console", async e => {
page.on("error", e => console.error("[Error]", e.message)); page.on("error", e => console.error("[Error]", e.message));
page.on("pageerror", e => { page.on("pageerror", e => {
if (e.message.includes("Sentry successfully disabled")) return;
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) { if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
console.error("[Page Error]", e.message); console.error("[Page Error]", e.message);
report.otherErrors.push(e.message); report.otherErrors.push(e.message);

View file

@ -44,6 +44,11 @@ export interface ProfileBadge {
position?: BadgePosition; position?: BadgePosition;
/** The badge name to display, Discord uses this. Required for component badges */ /** The badge name to display, Discord uses this. Required for component badges */
key?: string; key?: string;
/**
* Allows dynamically returning multiple badges
*/
getBadges?(userInfo: BadgeUserArgs): ProfileBadge[];
} }
const Badges = new Set<ProfileBadge>(); const Badges = new Set<ProfileBadge>();
@ -73,9 +78,16 @@ export function _getBadges(args: BadgeUserArgs) {
const badges = [] as ProfileBadge[]; const badges = [] as ProfileBadge[];
for (const badge of Badges) { for (const badge of Badges) {
if (!badge.shouldShow || badge.shouldShow(args)) { if (!badge.shouldShow || badge.shouldShow(args)) {
const b = badge.getBadges
? badge.getBadges(args).map(b => {
b.component &&= ErrorBoundary.wrap(b.component, { noop: true });
return b;
})
: [{ ...badge, ...args }];
badge.position === BadgePosition.START badge.position === BadgePosition.START
? badges.unshift({ ...badge, ...args }) ? badges.unshift(...b)
: badges.push({ ...badge, ...args }); : badges.push(...b);
} }
} }
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.userId); const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.userId);

View file

@ -19,6 +19,8 @@
import * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
import { openNotificationSettingsModal } from "@components/VencordSettings/NotificationSettings";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
@ -170,24 +172,31 @@ function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void
</ModalContent> </ModalContent>
<ModalFooter> <ModalFooter>
<Button <Flex>
disabled={log.length === 0} <Button onClick={openNotificationSettingsModal}>
onClick={() => { Notification Settings
Alerts.show({ </Button>
title: "Are you sure?",
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`, <Button
async onConfirm() { disabled={log.length === 0}
await DataStore.set(KEY, []); color={Button.Colors.RED}
signals.forEach(x => x()); onClick={() => {
}, Alerts.show({
confirmText: "Do it!", title: "Are you sure?",
confirmColor: "vc-notification-log-danger-btn", body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
cancelText: "Nevermind" async onConfirm() {
}); await DataStore.set(KEY, []);
}} signals.forEach(x => x());
> },
Clear Notification Log confirmText: "Do it!",
</Button> confirmColor: "vc-notification-log-danger-btn",
cancelText: "Nevermind"
});
}}
>
Clear Notification Log
</Button>
</Flex>
</ModalFooter> </ModalFooter>
</ModalRoot> </ModalRoot>
); );

View file

@ -129,7 +129,7 @@ export const SettingsStore = new SettingsStoreClass(settings, {
if (path === "plugins" && key in plugins) if (path === "plugins" && key in plugins)
return target[key] = { return target[key] = {
enabled: IS_REPORTER ?? plugins[key].required ?? plugins[key].enabledByDefault ?? false enabled: IS_REPORTER || plugins[key].required || plugins[key].enabledByDefault || false
}; };
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve // Since the property is not set, check if this is a plugin's setting and if so, try to resolve

View file

@ -31,10 +31,20 @@ export interface ExpandableHeaderProps {
headerText: string; headerText: string;
children: React.ReactNode; children: React.ReactNode;
buttons?: React.ReactNode[]; buttons?: React.ReactNode[];
forceOpen?: boolean;
} }
export function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) { export function ExpandableHeader({
const [showContent, setShowContent] = useState(defaultState); children,
onMoreClick,
buttons,
moreTooltipText,
onDropDownClick,
headerText,
defaultState = false,
forceOpen = false,
}: ExpandableHeaderProps) {
const [showContent, setShowContent] = useState(defaultState || forceOpen);
return ( return (
<> <>
@ -90,6 +100,7 @@ export function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipTe
setShowContent(v => !v); setShowContent(v => !v);
onDropDownClick?.(showContent); onDropDownClick?.(showContent);
}} }}
disabled={forceOpen}
> >
<svg <svg
width="24" width="24"

28
src/components/Grid.tsx Normal file
View file

@ -0,0 +1,28 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CSSProperties } from "react";
interface Props {
columns: number;
gap?: string;
inline?: boolean;
}
export function Grid(props: Props & JSX.IntrinsicElements["div"]) {
const style: CSSProperties = {
display: props.inline ? "inline-grid" : "grid",
gridTemplateColumns: `repeat(${props.columns}, 1fr)`,
gap: props.gap,
...props.style
};
return (
<div {...props} style={style}>
{props.children}
</div>
);
}

View file

@ -18,19 +18,17 @@
import "./iconStyles.css"; import "./iconStyles.css";
import { getTheme, Theme } from "@utils/discord";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { i18n } from "@webpack/common"; import { i18n } from "@webpack/common";
import type { PropsWithChildren, SVGProps } from "react"; import type { PropsWithChildren } from "react";
interface BaseIconProps extends IconProps { interface BaseIconProps extends IconProps {
viewBox: string; viewBox: string;
} }
interface IconProps extends SVGProps<SVGSVGElement> { type IconProps = JSX.IntrinsicElements["svg"];
className?: string; type ImageProps = JSX.IntrinsicElements["img"];
height?: string | number;
width?: string | number;
}
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) { function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
return ( return (
@ -290,3 +288,142 @@ export function NoEntrySignIcon(props: IconProps) {
</Icon> </Icon>
); );
} }
export function SafetyIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-safety-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M4.27 5.22A2.66 2.66 0 0 0 3 7.5v2.3c0 5.6 3.3 10.68 8.42 12.95.37.17.79.17 1.16 0A14.18 14.18 0 0 0 21 9.78V7.5c0-.93-.48-1.78-1.27-2.27l-6.17-3.76a3 3 0 0 0-3.12 0L4.27 5.22ZM6 7.68l6-3.66V12H6.22C6.08 11.28 6 10.54 6 9.78v-2.1Zm6 12.01V12h5.78A11.19 11.19 0 0 1 12 19.7Z"
/>
</Icon>
);
}
export function NotesIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-notes-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M8 3C7.44771 3 7 3.44772 7 4V5C7 5.55228 7.44772 6 8 6H16C16.5523 6 17 5.55228 17 5V4C17 3.44772 16.5523 3 16 3H15.1245C14.7288 3 14.3535 2.82424 14.1002 2.52025L13.3668 1.64018C13.0288 1.23454 12.528 1 12 1C11.472 1 10.9712 1.23454 10.6332 1.64018L9.8998 2.52025C9.64647 2.82424 9.27121 3 8.8755 3H8Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
fill="currentColor"
d="M19 4.49996V4.99996C19 6.65681 17.6569 7.99996 16 7.99996H8C6.34315 7.99996 5 6.65681 5 4.99996V4.49996C5 4.22382 4.77446 3.99559 4.50209 4.04109C3.08221 4.27826 2 5.51273 2 6.99996V19C2 20.6568 3.34315 22 5 22H19C20.6569 22 22 20.6568 22 19V6.99996C22 5.51273 20.9178 4.27826 19.4979 4.04109C19.2255 3.99559 19 4.22382 19 4.49996ZM8 12C7.44772 12 7 12.4477 7 13C7 13.5522 7.44772 14 8 14H16C16.5523 14 17 13.5522 17 13C17 12.4477 16.5523 12 16 12H8ZM7 17C7 16.4477 7.44772 16 8 16H13C13.5523 16 14 16.4477 14 17C14 17.5522 13.5523 18 13 18H8C7.44772 18 7 17.5522 7 17Z"
/>
</Icon>
);
}
export function FolderIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-folder-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 5a3 3 0 0 1 3-3h3.93a2 2 0 0 1 1.66.9L12 5h7a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5Z"
/>
</Icon>
);
}
export function LogIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-log-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M3.11 8H6v10.82c0 .86.37 1.68 1 2.27.46.43 1.02.71 1.63.84A1 1 0 0 0 9 22h10a4 4 0 0 0 4-4v-1a2 2 0 0 0-2-2h-1V5a3 3 0 0 0-3-3H4.67c-.87 0-1.7.32-2.34.9-.63.6-1 1.42-1 2.28 0 .71.3 1.35.52 1.75a5.35 5.35 0 0 0 .48.7l.01.01h.01L3.11 7l-.76.65a1 1 0 0 0 .76.35Zm1.56-4c-.38 0-.72.14-.97.37-.24.23-.37.52-.37.81a1.69 1.69 0 0 0 .3.82H6v-.83c0-.29-.13-.58-.37-.8C5.4 4.14 5.04 4 4.67 4Zm5 13a3.58 3.58 0 0 1 0 3H19a2 2 0 0 0 2-2v-1H9.66ZM3.86 6.35ZM11 8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm-1 5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1Z"
/>
</Icon>
);
}
export function RestartIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-restart-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M4 12a8 8 0 0 1 14.93-4H15a1 1 0 1 0 0 2h6a1 1 0 0 0 1-1V3a1 1 0 1 0-2 0v3a9.98 9.98 0 0 0-18 6 10 10 0 0 0 16.29 7.78 1 1 0 0 0-1.26-1.56A8 8 0 0 1 4 12Z"
/>
</Icon>
);
}
export function PaintbrushIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-paintbrush-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M15.35 7.24C15.9 6.67 16 5.8 16 5a3 3 0 1 1 3 3c-.8 0-1.67.09-2.24.65a1.5 1.5 0 0 0 0 2.11l1.12 1.12a3 3 0 0 1 0 4.24l-5 5a3 3 0 0 1-4.25 0l-5.76-5.75a3 3 0 0 1 0-4.24l4.04-4.04.97-.97a3 3 0 0 1 4.24 0l1.12 1.12c.58.58 1.52.58 2.1 0ZM6.9 9.9 4.3 12.54a1 1 0 0 0 0 1.42l2.17 2.17.83-.84a1 1 0 0 1 1.42 1.42l-.84.83.59.59 1.83-1.84a1 1 0 0 1 1.42 1.42l-1.84 1.83.17.17a1 1 0 0 0 1.42 0l2.63-2.62L6.9 9.9Z"
/>
</Icon>
);
}
export function PencilIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-pencil-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z"
/>
</Icon>
);
}
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
export function GithubIcon(props: ImageProps) {
const src = getTheme() === Theme.Light
? GithubIconLight
: GithubIconDark;
return <img {...props} src={src} />;
}
export function WebsiteIcon(props: ImageProps) {
const src = getTheme() === Theme.Light
? WebsiteIconLight
: WebsiteIconDark;
return <img {...props} src={src} />;
}

View file

@ -6,22 +6,16 @@
import "./LinkIconButton.css"; import "./LinkIconButton.css";
import { getTheme, Theme } from "@utils/discord";
import { MaskedLink, Tooltip } from "@webpack/common"; import { MaskedLink, Tooltip } from "@webpack/common";
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg"; import { GithubIcon, WebsiteIcon } from "..";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
export function GithubIcon() { export function GithubLinkIcon() {
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark; return <GithubIcon aria-hidden className={"vc-settings-modal-link-icon"} />;
return <img src={src} aria-hidden className={"vc-settings-modal-link-icon"} />;
} }
export function WebsiteIcon() { export function WebsiteLinkIcon() {
const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark; return <WebsiteIcon aria-hidden className={"vc-settings-modal-link-icon"} />;
return <img src={src} aria-hidden className={"vc-settings-modal-link-icon"} />;
} }
interface Props { interface Props {
@ -41,5 +35,5 @@ function LinkIcon({ text, href, Icon }: Props & { Icon: React.ComponentType; })
); );
} }
export const WebsiteButton = (props: Props) => <LinkIcon {...props} Icon={WebsiteIcon} />; export const WebsiteButton = (props: Props) => <LinkIcon {...props} Icon={WebsiteLinkIcon} />;
export const GithubButton = (props: Props) => <LinkIcon {...props} Icon={GithubIcon} />; export const GithubButton = (props: Props) => <LinkIcon {...props} Icon={GithubLinkIcon} />;

View file

@ -27,7 +27,7 @@ import { gitRemote } from "@shared/vencordUserAgent";
import { proxyLazy } from "@utils/lazy"; import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc"; import { classes, isObjectEmpty } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { OptionType, Plugin } from "@utils/types"; import { OptionType, Plugin } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common"; import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
@ -310,3 +310,13 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</ModalRoot> </ModalRoot>
); );
} }
export function openPluginModal(plugin: Plugin, onRestartNeeded?: (pluginName: string) => void) {
openModal(modalProps => (
<PluginModal
{...modalProps}
plugin={plugin}
onRestartNeeded={() => onRestartNeeded?.(plugin.name)}
/>
));
}

View file

@ -23,7 +23,7 @@ import { showNotice } from "@api/Notices";
import { Settings, useSettings } from "@api/Settings"; import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { CogWheel, InfoIcon } from "@components/Icons"; import { CogWheel, InfoIcon } from "@components/Icons";
import PluginModal from "@components/PluginSettings/PluginModal"; import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { AddonCard } from "@components/VencordSettings/AddonCard"; import { AddonCard } from "@components/VencordSettings/AddonCard";
import { SettingsTab } from "@components/VencordSettings/shared"; import { SettingsTab } from "@components/VencordSettings/shared";
import { ChangeList } from "@utils/ChangeList"; import { ChangeList } from "@utils/ChangeList";
@ -31,7 +31,6 @@ import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc"; import { classes, isObjectEmpty } from "@utils/misc";
import { openModalLazy } from "@utils/modal";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { Plugin } from "@utils/types"; import { Plugin } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
@ -45,7 +44,7 @@ const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() =>
const cl = classNameFactory("vc-plugins-"); const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189"); const logger = new Logger("PluginSettings", "#a6d189");
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper"); const InputStyles = findByPropsLazy("inputWrapper", "inputDefault", "error");
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled"); const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
@ -96,14 +95,6 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
const isEnabled = () => settings.enabled ?? false; const isEnabled = () => settings.enabled ?? false;
function openModal() {
openModalLazy(async () => {
return modalProps => {
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
};
});
}
function toggleEnabled() { function toggleEnabled() {
const wasEnabled = isEnabled(); const wasEnabled = isEnabled();
@ -160,7 +151,11 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
infoButton={ infoButton={
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}> <button
role="switch"
onClick={() => openPluginModal(plugin, onRestartNeeded)}
className={classes(ButtonClasses.button, cl("info-button"))}
>
{plugin.options && !isObjectEmpty(plugin.options) {plugin.options && !isObjectEmpty(plugin.options)
? <CogWheel /> ? <CogWheel />
: <InfoIcon />} : <InfoIcon />}
@ -339,8 +334,8 @@ export default function PluginSettings() {
Filters Filters
</Forms.FormTitle> </Forms.FormTitle>
<div className={cl("filter-controls")}> <div className={classes(Margins.bottom20, cl("filter-controls"))}>
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} /> <TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} />
<div className={InputStyles.inputWrapper}> <div className={InputStyles.inputWrapper}>
<Select <Select
options={[ options={[
@ -353,6 +348,7 @@ export default function PluginSettings() {
select={onStatusChange} select={onStatusChange}
isSelected={v => v === searchValue.status} isSelected={v => v === searchValue.status}
closeOnSelect={true} closeOnSelect={true}
className={InputStyles.inputDefault}
/> />
</div> </div>
</div> </div>

View file

@ -19,6 +19,7 @@
import { showNotification } from "@api/Notifications"; import { showNotification } from "@api/Notifications";
import { Settings, useSettings } from "@api/Settings"; import { Settings, useSettings } from "@api/Settings";
import { CheckedTextInput } from "@components/CheckedTextInput"; import { CheckedTextInput } from "@components/CheckedTextInput";
import { Grid } from "@components/Grid";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud"; import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
@ -85,7 +86,9 @@ function SettingsSyncSection() {
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
disabled={!sectionEnabled} disabled={!sectionEnabled}
onClick={() => putCloudSettings(true)} onClick={() => putCloudSettings(true)}
>Sync to Cloud</Button> >
Sync to Cloud
</Button>
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!"> <Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
{({ onMouseLeave, onMouseEnter }) => ( {({ onMouseLeave, onMouseEnter }) => (
<Button <Button
@ -95,7 +98,9 @@ function SettingsSyncSection() {
color={Button.Colors.RED} color={Button.Colors.RED}
disabled={!sectionEnabled} disabled={!sectionEnabled}
onClick={() => getCloudSettings(true, true)} onClick={() => getCloudSettings(true, true)}
>Sync from Cloud</Button> >
Sync from Cloud
</Button>
)} )}
</Tooltip> </Tooltip>
<Button <Button
@ -103,7 +108,9 @@ function SettingsSyncSection() {
color={Button.Colors.RED} color={Button.Colors.RED}
disabled={!sectionEnabled} disabled={!sectionEnabled}
onClick={() => deleteCloudSettings()} onClick={() => deleteCloudSettings()}
>Delete Cloud Settings</Button> >
Delete Cloud Settings
</Button>
</div> </div>
</Forms.FormSection> </Forms.FormSection>
); );
@ -124,7 +131,12 @@ function CloudTab() {
<Switch <Switch
key="backend" key="backend"
value={settings.cloud.authenticated} value={settings.cloud.authenticated}
onChange={v => { v && authorizeCloud(); if (!v) settings.cloud.authenticated = v; }} onChange={v => {
if (v)
authorizeCloud();
else
settings.cloud.authenticated = v;
}}
note="This will request authorization if you have not yet set up cloud integrations." note="This will request authorization if you have not yet set up cloud integrations."
> >
Enable Cloud Integrations Enable Cloud Integrations
@ -136,23 +148,43 @@ function CloudTab() {
<CheckedTextInput <CheckedTextInput
key="backendUrl" key="backendUrl"
value={settings.cloud.url} value={settings.cloud.url}
onChange={v => { settings.cloud.url = v; settings.cloud.authenticated = false; deauthorizeCloud(); }} onChange={async v => {
settings.cloud.url = v;
settings.cloud.authenticated = false;
deauthorizeCloud();
}}
validate={validateUrl} validate={validateUrl}
/> />
<Button
className={Margins.top8} <Grid columns={2} gap="1em" className={Margins.top8}>
size={Button.Sizes.MEDIUM} <Button
color={Button.Colors.RED} size={Button.Sizes.MEDIUM}
disabled={!settings.cloud.authenticated} disabled={!settings.cloud.authenticated}
onClick={() => Alerts.show({ onClick={async () => {
title: "Are you sure?", await deauthorizeCloud();
body: "Once your data is erased, we cannot recover it. There's no going back!", settings.cloud.authenticated = false;
onConfirm: eraseAllData, await authorizeCloud();
confirmText: "Erase it!", }}
confirmColor: "vc-cloud-erase-data-danger-btn", >
cancelText: "Nevermind" Reauthorise
})} </Button>
>Erase All Data</Button> <Button
size={Button.Sizes.MEDIUM}
color={Button.Colors.RED}
disabled={!settings.cloud.authenticated}
onClick={() => Alerts.show({
title: "Are you sure?",
body: "Once your data is erased, we cannot recover it. There's no going back!",
onConfirm: eraseAllData,
confirmText: "Erase it!",
confirmColor: "vc-cloud-erase-data-danger-btn",
cancelText: "Nevermind"
})}
>
Erase All Data
</Button>
</Grid>
<Forms.FormDivider className={Margins.top16} /> <Forms.FormDivider className={Margins.top16} />
</Forms.FormSection > </Forms.FormSection >
<SettingsSyncSection /> <SettingsSyncSection />

View file

@ -0,0 +1,106 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useSettings } from "@api/Settings";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Forms, Select, Slider, Text } from "@webpack/common";
import { ErrorCard } from "..";
export function NotificationSettings() {
const settings = useSettings().notifications;
return (
<div style={{ padding: "1em 0" }}>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{settings.useNative !== "never" && Notification?.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
</ErrorCard>
)}
<Forms.FormText className={Margins.bottom8}>
Some plugins may show you notifications. These come in two styles:
<ul>
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
</ul>
</Forms.FormText>
<Select
placeholder="Notification Style"
options={[
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" },
] satisfies Array<{ value: typeof settings["useNative"]; } & Record<string, any>>}
closeOnSelect={true}
select={v => settings.useNative = v}
isSelected={v => v === settings.useNative}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
<Select
isDisabled={settings.useNative === "always"}
placeholder="Notification Position"
options={[
{ label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" },
] satisfies Array<{ value: typeof settings["position"]; } & Record<string, any>>}
select={v => settings.position = v}
isSelected={v => v === settings.position}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
<Slider
disabled={settings.useNative === "always"}
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
minValue={0}
maxValue={20_000}
initialValue={settings.timeout}
onValueChange={v => settings.timeout = v}
onValueRender={v => (v / 1000).toFixed(2) + "s"}
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>
The amount of notifications to save in the log until old ones are removed.
Set to <code>0</code> to disable Notification log and <code></code> to never automatically remove old Notifications
</Forms.FormText>
<Slider
markers={[0, 25, 50, 75, 100, 200]}
minValue={0}
maxValue={200}
stickToMarkers={true}
initialValue={settings.logLimit}
onValueChange={v => settings.logLimit = v}
onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v}
/>
</div>
);
}
export function openNotificationSettingsModal() {
openModal(props => (
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Settings</Text>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
<NotificationSettings />
</ModalContent>
</ModalRoot>
));
}

View file

@ -19,21 +19,21 @@
import { useSettings } from "@api/Settings"; import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons"; import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import PluginModal from "@components/PluginSettings/PluginModal"; import { openPluginModal } from "@components/PluginSettings/PluginModal";
import type { UserThemeHeader } from "@main/themes"; import type { UserThemeHeader } from "@main/themes";
import { openInviteModal } from "@utils/discord"; import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { openModal } from "@utils/modal";
import { showItemInFolder } from "@utils/native"; import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findLazy } from "@webpack"; import { findByPropsLazy, findLazy } from "@webpack";
import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react"; import type { ComponentType, Ref, SyntheticEvent } from "react";
import { AddonCard } from "./AddonCard"; import { AddonCard } from "./AddonCard";
import { QuickAction, QuickActionCard } from "./quickActions";
import { SettingsTab, wrapTab } from "./shared"; import { SettingsTab, wrapTab } from "./shared";
type FileInput = ComponentType<{ type FileInput = ComponentType<{
@ -213,60 +213,52 @@ function ThemesTab() {
</Card> </Card>
<Forms.FormSection title="Local Themes"> <Forms.FormSection title="Local Themes">
<Card className="vc-settings-quick-actions-card"> <QuickActionCard>
<> <>
{IS_WEB ? {IS_WEB ?
( (
<Button <QuickAction
size={Button.Sizes.SMALL} text={
disabled={themeDirPending} <span style={{ position: "relative" }}>
> Upload Theme
Upload Theme <FileInput
<FileInput ref={fileInputRef}
ref={fileInputRef} onChange={onFileUpload}
onChange={onFileUpload} multiple={true}
multiple={true} filters={[{ extensions: ["css"] }]}
filters={[{ extensions: ["css"] }]} />
/> </span>
</Button> }
Icon={PlusIcon}
/>
) : ( ) : (
<Button <QuickAction
onClick={() => showItemInFolder(themeDir!)} text="Open Themes Folder"
size={Button.Sizes.SMALL} action={() => showItemInFolder(themeDir!)}
disabled={themeDirPending} disabled={themeDirPending}
> Icon={FolderIcon}
Open Themes Folder />
</Button>
)} )}
<Button <QuickAction
onClick={refreshLocalThemes} text="Load missing Themes"
size={Button.Sizes.SMALL} action={refreshLocalThemes}
> Icon={RestartIcon}
Load missing Themes />
</Button> <QuickAction
<Button text="Edit QuickCSS"
onClick={() => VencordNative.quickCss.openEditor()} action={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL} Icon={PaintbrushIcon}
> />
Edit QuickCSS
</Button>
{Vencord.Settings.plugins.ClientTheme.enabled && ( {Vencord.Settings.plugins.ClientTheme.enabled && (
<Button <QuickAction
onClick={() => openModal(modalProps => ( text="Edit ClientTheme"
<PluginModal action={() => openPluginModal(Vencord.Plugins.plugins.ClientTheme)}
{...modalProps} Icon={PencilIcon}
plugin={Vencord.Plugins.plugins.ClientTheme} />
onRestartNeeded={() => { }}
/>
))}
size={Button.Sizes.SMALL}
>
Edit ClientTheme
</Button>
)} )}
</> </>
</Card> </QuickActionCard>
<div className={cl("grid")}> <div className={cl("grid")}>
{userThemes?.map(theme => ( {userThemes?.map(theme => (

View file

@ -17,16 +17,20 @@
*/ */
import { openNotificationLogModal } from "@api/Notifications/notificationLog"; import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { Settings, useSettings } from "@api/Settings"; import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton"; import DonateButton from "@components/DonateButton";
import { ErrorCard } from "@components/ErrorCard"; import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { gitRemote } from "@shared/vencordUserAgent";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { identity } from "@utils/misc"; import { identity } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native"; import { relaunch, showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common"; import { Button, Card, Forms, React, Select, Switch } from "@webpack/common";
import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "..";
import { openNotificationSettingsModal } from "./NotificationSettings";
import { QuickAction, QuickActionCard } from "./quickActions";
import { SettingsTab, wrapTab } from "./shared"; import { SettingsTab, wrapTab } from "./shared";
const cl = classNameFactory("vc-settings-"); const cl = classNameFactory("vc-settings-");
@ -38,6 +42,7 @@ type KeysOfType<Object, Type> = {
[K in keyof Object]: Object[K] extends Type ? K : never; [K in keyof Object]: Object[K] extends Type ? K : never;
}[keyof Object]; }[keyof Object];
function VencordSettings() { function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, { const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
fallbackValue: "Loading..." fallbackValue: "Loading..."
@ -78,7 +83,7 @@ function VencordSettings() {
!IS_WEB && { !IS_WEB && {
key: "transparent", key: "transparent",
title: "Enable window transparency.", title: "Enable window transparency.",
note: "You need a theme that supports transparency or this will do nothing. Will stop the window from being resizable. Requires a full restart" note: "You need a theme that supports transparency or this will do nothing. WILL STOP THE WINDOW FROM BEING RESIZABLE!! Requires a full restart"
}, },
!IS_WEB && isWindows && { !IS_WEB && isWindows && {
key: "winCtrlQ", key: "winCtrlQ",
@ -96,45 +101,53 @@ function VencordSettings() {
<SettingsTab title="Vencord Settings"> <SettingsTab title="Vencord Settings">
<DonateCard image={donateImage} /> <DonateCard image={donateImage} />
<Forms.FormSection title="Quick Actions"> <Forms.FormSection title="Quick Actions">
<Card className={cl("quick-actions-card")}> <QuickActionCard>
<React.Fragment> <QuickAction
{!IS_WEB && ( Icon={LogIcon}
<Button text="Notification Log"
onClick={relaunch} action={openNotificationLogModal}
size={Button.Sizes.SMALL}> />
Restart Client <QuickAction
</Button> Icon={PaintbrushIcon}
)} text="Edit QuickCSS"
<Button action={() => VencordNative.quickCss.openEditor()}
onClick={() => VencordNative.quickCss.openEditor()} />
size={Button.Sizes.SMALL} {!IS_WEB && (
disabled={settingsDir === "Loading..."}> <QuickAction
Open QuickCSS File Icon={RestartIcon}
</Button> text="Relaunch Discord"
{!IS_WEB && ( action={relaunch}
<Button />
onClick={() => showItemInFolder(settingsDir)} )}
size={Button.Sizes.SMALL} {!IS_WEB && (
disabled={settingsDirPending}> <QuickAction
Open Settings Folder Icon={FolderIcon}
</Button> text="Open Settings Folder"
)} action={() => showItemInFolder(settingsDir)}
<Button />
onClick={() => VencordNative.native.openExternal("https://github.com/Vendicated/Vencord")} )}
size={Button.Sizes.SMALL} <QuickAction
disabled={settingsDirPending}> Icon={GithubIcon}
Open in GitHub text="View Source Code"
</Button> action={() => VencordNative.native.openExternal("https://github.com/" + gitRemote)}
</React.Fragment> />
</Card> </QuickActionCard>
</Forms.FormSection> </Forms.FormSection>
<Forms.FormDivider /> <Forms.FormDivider />
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5"> <Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
<Forms.FormText className={Margins.bottom20}> <Forms.FormText className={Margins.bottom20} style={{ color: "var(--text-muted)" }}>
Hint: You can change the position of this settings section in the settings of the "Settings" plugin! Hint: You can change the position of this settings section in the
{" "}<Button
look={Button.Looks.BLANK}
style={{ color: "var(--text-link)", display: "inline-block" }}
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
>
settings of the Settings plugin
</Button>!
</Forms.FormText> </Forms.FormText>
{Switches.map(s => s && ( {Switches.map(s => s && (
<Switch <Switch
key={s.key} key={s.key}
@ -212,94 +225,20 @@ function VencordSettings() {
serialize={identity} /> serialize={identity} />
</>} </>}
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />} <Forms.FormSection className={Margins.top16} title="Vencord Notifications" tag="h5">
<Flex>
<Button onClick={openNotificationSettingsModal}>
Notification Settings
</Button>
<Button onClick={openNotificationLogModal}>
View Notification Log
</Button>
</Flex>
</Forms.FormSection>
</SettingsTab> </SettingsTab>
); );
} }
function NotificationSection({ settings }: { settings: typeof Settings["notifications"]; }) {
return (
<>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{settings.useNative !== "never" && Notification?.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
</ErrorCard>
)}
<Forms.FormText className={Margins.bottom8}>
Some plugins may show you notifications. These come in two styles:
<ul>
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
</ul>
</Forms.FormText>
<Select
placeholder="Notification Style"
options={[
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" },
] satisfies Array<{ value: typeof settings["useNative"]; } & Record<string, any>>}
closeOnSelect={true}
select={v => settings.useNative = v}
isSelected={v => v === settings.useNative}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
<Select
isDisabled={settings.useNative === "always"}
placeholder="Notification Position"
options={[
{ label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" },
] satisfies Array<{ value: typeof settings["position"]; } & Record<string, any>>}
select={v => settings.position = v}
isSelected={v => v === settings.position}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
<Slider
disabled={settings.useNative === "always"}
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
minValue={0}
maxValue={20_000}
initialValue={settings.timeout}
onValueChange={v => settings.timeout = v}
onValueRender={v => (v / 1000).toFixed(2) + "s"}
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>
The amount of notifications to save in the log until old ones are removed.
Set to <code>0</code> to disable Notification log and <code></code> to never automatically remove old Notifications
</Forms.FormText>
<Slider
markers={[0, 25, 50, 75, 100, 200]}
minValue={0}
maxValue={200}
stickToMarkers={true}
initialValue={settings.logLimit}
onValueChange={v => settings.logLimit = v}
onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v}
/>
<Button
onClick={openNotificationLogModal}
disabled={settings.logLimit === 0}
>
Open Notification Log
</Button>
</>
);
}
interface DonateCardProps { interface DonateCardProps {
image: string; image: string;
} }

View file

@ -0,0 +1,33 @@
.vc-settings-quickActions-card {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, max-content));
gap: 0.5em;
justify-content: center;
padding: 0.5em 0;
margin-bottom: 1em;
}
.vc-settings-quickActions-pill {
all: unset;
background: var(--background-secondary);
color: var(--header-secondary);
display: flex;
align-items: center;
gap: 0.5em;
padding: 8px 12px;
border-radius: 9999px;
}
.vc-settings-quickActions-pill:hover {
background: var(--background-secondary-alt);
}
.vc-settings-quickActions-pill:focus-visible {
outline: 2px solid var(--focus-primary);
outline-offset: 2px;
}
.vc-settings-quickActions-img {
width: 24px;
height: 24px;
}

View file

@ -0,0 +1,39 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./quickActions.css";
import { classNameFactory } from "@api/Styles";
import { Card } from "@webpack/common";
import type { ComponentType, PropsWithChildren, ReactNode } from "react";
const cl = classNameFactory("vc-settings-quickActions-");
export interface QuickActionProps {
Icon: ComponentType<{ className?: string; }>;
text: ReactNode;
action?: () => void;
disabled?: boolean;
}
export function QuickAction(props: QuickActionProps) {
const { Icon, action, text, disabled } = props;
return (
<button className={cl("pill")} onClick={action} disabled={disabled}>
<Icon className={cl("img")} />
{text}
</button>
);
}
export function QuickActionCard(props: PropsWithChildren) {
return (
<Card className={cl("card")}>
{props.children}
</Card>
);
}

View file

@ -10,17 +10,6 @@
margin-bottom: -2px; margin-bottom: -2px;
} }
.vc-settings-quick-actions-card {
padding: 1em;
display: flex;
gap: 1em;
align-items: center;
justify-content: space-evenly;
flex-grow: 1;
flex-flow: row wrap;
margin-bottom: 1em;
}
.vc-settings-donate { .vc-settings-donate {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View file

@ -136,6 +136,8 @@ export default definePlugin({
}, },
getBadges(props: { userId: string; user?: User; guildId: string; }) { getBadges(props: { userId: string; user?: User; guildId: string; }) {
if (!props) return [];
try { try {
props.userId ??= props.user?.id!; props.userId ??= props.user?.id!;

View file

@ -18,7 +18,8 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, StartAt } from "@utils/types";
const settings = definePluginSettings({ const settings = definePluginSettings({
disableAnalytics: { disableAnalytics: {
@ -46,13 +47,6 @@ export default definePlugin({
replace: "()=>{}", replace: "()=>{}",
}, },
}, },
{
find: "window.DiscordSentry=",
replacement: {
match: /^.+$/,
replace: "()=>{}",
}
},
{ {
find: ".METRICS,", find: ".METRICS,",
replacement: [ replacement: [
@ -74,5 +68,66 @@ export default definePlugin({
replace: "getDebugLogging(){return false;" replace: "getDebugLogging(){return false;"
} }
}, },
] ],
startAt: StartAt.Init,
start() {
// Sentry is initialized in its own WebpackInstance.
// It has everything it needs preloaded, so, it doesn't include any chunk loading functionality.
// Because of that, its WebpackInstance doesnt export wreq.m or wreq.c
// To circuvent this and disable Sentry we are gonna hook when wreq.g of its WebpackInstance is set.
// When that happens we are gonna forcefully throw an error and abort everything.
Object.defineProperty(Function.prototype, "g", {
configurable: true,
set(v: any) {
Object.defineProperty(this, "g", {
value: v,
configurable: true,
enumerable: true,
writable: true
});
// Ensure this is most likely the Sentry WebpackInstance.
// Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: <g></g>) to include it
const { stack } = new Error();
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || !String(this).includes("exports:{}") || this.c != null) {
return;
}
const assetPath = stack?.match(/\/assets\/.+?\.js/)?.[0];
if (!assetPath) {
return;
}
const srcRequest = new XMLHttpRequest();
srcRequest.open("GET", assetPath, false);
srcRequest.send();
// Final condition to see if this is the Sentry WebpackInstance
if (!srcRequest.responseText.includes("window.DiscordSentry=")) {
return;
}
new Logger("NoTrack", "#8caaee").info("Disabling Sentry by erroring its WebpackInstance");
Reflect.deleteProperty(Function.prototype, "g");
Reflect.deleteProperty(window, "DiscordSentry");
throw new Error("Sentry successfully disabled");
}
});
Object.defineProperty(window, "DiscordSentry", {
configurable: true,
set() {
new Logger("NoTrack", "#8caaee").error("Failed to disable Sentry. Falling back to deleting window.DiscordSentry");
Reflect.deleteProperty(Function.prototype, "g");
Reflect.deleteProperty(window, "DiscordSentry");
}
});
}
}); });

View file

@ -17,6 +17,7 @@
*/ */
import { addAccessory } from "@api/MessageAccessories"; import { addAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings"; import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
@ -32,12 +33,12 @@ import { onlyOnce } from "@utils/onlyOnce";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { checkForUpdates, isOutdated, update } from "@utils/updater"; import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Toasts, UserStore } from "@webpack/common"; import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
import plugins, { PluginMeta } from "~plugins"; import plugins, { PluginMeta } from "~plugins";
import settings from "./settings"; import SettingsPlugin from "./settings";
const VENCORD_GUILD_ID = "1015060230222131221"; const VENCORD_GUILD_ID = "1015060230222131221";
const VENBOT_USER_ID = "1017176847865352332"; const VENBOT_USER_ID = "1017176847865352332";
@ -86,7 +87,7 @@ async function generateDebugInfoMessage() {
const info = { const info = {
Vencord: Vencord:
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` + `v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
`${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`, `${SettingsPlugin.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
Client: `${RELEASE_CHANNEL} ~ ${client}`, Client: `${RELEASE_CHANNEL} ~ ${client}`,
Platform: window.navigator.platform Platform: window.navigator.platform
}; };
@ -132,6 +133,10 @@ function generatePluginList() {
const checkForUpdatesOnce = onlyOnce(checkForUpdates); const checkForUpdatesOnce = onlyOnce(checkForUpdates);
const settings = definePluginSettings({}).withPrivateSettings<{
dismissedDevBuildWarning?: boolean;
}>();
export default definePlugin({ export default definePlugin({
name: "SupportHelper", name: "SupportHelper",
required: true, required: true,
@ -139,6 +144,8 @@ export default definePlugin({
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["CommandsAPI", "UserSettingsAPI", "MessageAccessoriesAPI"], dependencies: ["CommandsAPI", "UserSettingsAPI", "MessageAccessoriesAPI"],
settings,
patches: [{ patches: [{
find: ".BEGINNING_DM.format", find: ".BEGINNING_DM.format",
replacement: { replacement: {
@ -207,17 +214,22 @@ export default definePlugin({
}); });
} }
const repo = await VencordNative.updater.getRepo(); if (!IS_STANDALONE && !settings.store.dismissedDevBuildWarning) {
if (repo.ok && !repo.value.includes("Vendicated/Vencord")) {
return Alerts.show({ return Alerts.show({
title: "Hold on!", title: "Hold on!",
body: <div> body: <div>
<Forms.FormText>You are using a fork of Vencord, which we do not provide support for!</Forms.FormText> <Forms.FormText>You are using a custom build of Vencord, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}> <Forms.FormText className={Margins.top8}>
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or We only provide support for <Link href="https://vencord.dev/download">official builds</Link>.
contact your package maintainer for support instead. Either <Link href="https://vencord.dev/download">switch to an official build</Link> or figure your issue out yourself.
</Forms.FormText> </Forms.FormText>
</div>
<Text variant="text-md/bold" className={Margins.top8}>You will be banned from receiving support if you ignore this rule.</Text>
</div>,
confirmText: "Understood",
secondaryConfirmText: "Don't show again",
onConfirmSecondary: () => settings.store.dismissedDevBuildWarning = true
}); });
} }
} }

View file

@ -0,0 +1,5 @@
# ConsoleJanitor
Disables annoying console messages/errors. This plugin mainly removes errors/warnings that happen all the time and noisy/spammy logging messages.
Some of the disabled messages include the "notosans-400-normalitalic" error and MessageActionCreators, Routing/Utils loggers.

View file

@ -0,0 +1,152 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const Noop = () => { };
const NoopLogger = {
logDangerously: Noop,
log: Noop,
verboseDangerously: Noop,
verbose: Noop,
info: Noop,
warn: Noop,
error: Noop,
trace: Noop,
time: Noop,
fileOnly: Noop
};
const settings = definePluginSettings({
disableNoisyLoggers: {
type: OptionType.BOOLEAN,
description: "Disable noisy loggers like the MessageActionCreators",
default: false,
restartNeeded: true
},
disableSpotifyLogger: {
type: OptionType.BOOLEAN,
description: "Disable the Spotify logger, which leaks account information and access token",
default: true,
restartNeeded: true
}
});
export default definePlugin({
name: "ConsoleJanitor",
description: "Disables annoying console messages/errors",
authors: [Devs.Nuckyz],
settings,
NoopLogger: () => NoopLogger,
patches: [
{
find: 'console.warn("Window state not initialized"',
replacement: {
match: /console\.warn\("Window state not initialized",\i\),/,
replace: ""
}
},
{
find: "is not a valid locale.",
replacement: {
match: /\i\.error\(""\.concat\(\i," is not a valid locale."\)\);/,
replace: ""
}
},
{
find: "notosans-400-normalitalic",
replacement: {
match: /,"notosans-.+?"/g,
replace: ""
}
},
{
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");',
all: true,
replacement: {
match: /console\.warn\("\[DEPRECATED\] Please use `subscribeWithSelector` middleware"\);/,
replace: ""
}
},
{
find: "RPCServer:WSS",
replacement: {
match: /\i\.error\("Error: "\.concat\((\i)\.message/,
replace: '!$1.message.includes("EADDRINUSE")&&$&'
}
},
{
find: "Tried getting Dispatch instance before instantiated",
replacement: {
match: /null==\i&&\i\.warn\("Tried getting Dispatch instance before instantiated"\),/,
replace: ""
}
},
{
find: "Unable to determine render window for element",
replacement: {
match: /console\.warn\("Unable to determine render window for element",\i\),/,
replace: ""
}
},
{
find: "failed to send analytics events",
replacement: {
match: /console\.error\("\[analytics\] failed to send analytics events query: "\.concat\(\i\)\)/,
replace: ""
}
},
{
find: "Slow dispatch on",
replacement: {
match: /\i\.totalTime>100&&\i\.verbose\("Slow dispatch on ".+?\)\);/,
replace: ""
}
},
...[
'("MessageActionCreators")', '("ChannelMessages")',
'("Routing/Utils")', '("RTCControlSocket")',
'("ConnectionEventFramerateReducer")', '("RTCLatencyTestManager")',
'("OverlayBridgeStore")', '("RPCServer:WSS")'
].map(logger => ({
find: logger,
predicate: () => settings.store.disableNoisyLoggers,
all: true,
replacement: {
match: new RegExp(String.raw`new \i\.\i${logger.replace(/([()])/g, "\\$1")}`),
replace: `$self.NoopLogger${logger}`
}
})),
{
find: '"Experimental codecs: "',
predicate: () => settings.store.disableNoisyLoggers,
replacement: {
match: /new \i\.\i\("Connection\("\.concat\(\i,"\)"\)\)/,
replace: "$self.NoopLogger()"
}
},
{
find: '"Handling ping: "',
predicate: () => settings.store.disableNoisyLoggers,
replacement: {
match: /new \i\.\i\("RTCConnection\("\.concat.+?\)\)(?=,)/,
replace: "$self.NoopLogger()"
}
},
{
find: '("Spotify")',
predicate: () => settings.store.disableSpotifyLogger,
replacement: {
match: /new \i\.\i\("Spotify"\)/,
replace: "$self.NoopLogger()"
}
}
]
});

View file

@ -40,9 +40,9 @@ export default definePlugin({
}), }),
patches: [ patches: [
{ {
find: ".ENTER&&(!", find: "!this.hasOpenCodeBlock()",
replacement: { replacement: {
match: /(?<=(\i)\.which===\i\.\i.ENTER&&).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/, match: /!(\i).shiftKey&&!(this.hasOpenCodeBlock\(\))&&\(.{0,100}?\)/,
replace: "$self.shouldSubmit($1, $2)" replace: "$self.shouldSubmit($1, $2)"
} }
} }

View file

@ -109,9 +109,9 @@ interface ProfileModalProps {
} }
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>('"ProfileCustomizationPreview"'); const ProfileModal = findComponentByCodeLazy<ProfileModalProps>("isTryItOutFlow:", "pendingThemeColors:", "pendingAvatarDecoration:", "EDIT_PROFILE_BANNER");
const requireColorPicker = extractAndLoadChunksLazy(["USER_SETTINGS_PROFILE_COLOR_DEFAULT_BUTTON.format"], /createPromise:\(\)=>\i\.\i\("?(.+?)"?\).then\(\i\.bind\(\i,"?(.+?)"?\)\)/); const requireColorPicker = extractAndLoadChunksLazy(["USER_SETTINGS_PROFILE_COLOR_DEFAULT_BUTTON.format"], /createPromise:\(\)=>\i\.\i(\("?.+?"?\)).then\(\i\.bind\(\i,"?(.+?)"?\)\)/);
export default definePlugin({ export default definePlugin({
name: "FakeProfileThemes", name: "FakeProfileThemes",

View file

@ -4,50 +4,64 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord"; import { getCurrentChannel } from "@utils/discord";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { classes } from "@utils/misc";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
import { Heading, React, RelationshipStore, Text } from "@webpack/common"; import { Heading, RelationshipStore, Text } from "@webpack/common";
const container = findByPropsLazy("memberSinceWrapper"); const containerWrapper = findByPropsLazy("memberSinceWrapper");
const container = findByPropsLazy("memberSince");
const getCreatedAtDate = findByCodeLazy('month:"short",day:"numeric"'); const getCreatedAtDate = findByCodeLazy('month:"short",day:"numeric"');
const locale = findByPropsLazy("getLocale"); const locale = findByPropsLazy("getLocale");
const lastSection = findByPropsLazy("lastSection"); const lastSection = findByPropsLazy("lastSection");
const section = findLazy((m: any) => m.section !== void 0 && Object.values(m).length === 1);
const cl = classNameFactory("vc-friendssince-");
export default definePlugin({ export default definePlugin({
name: "FriendsSince", name: "FriendsSince",
description: "Shows when you became friends with someone in the user popout", description: "Shows when you became friends with someone in the user popout",
authors: [Devs.Elvyra], authors: [Devs.Elvyra, Devs.Antti],
patches: [ patches: [
// User popup // User popup - old layout
{ {
find: ".USER_PROFILE}};return", find: ".USER_PROFILE}};return",
replacement: { replacement: {
match: /,{userId:(\i.id).{0,30}}\)/, match: /,{userId:(\i.id).{0,30}}\)/,
replace: "$&,$self.friendsSince({ userId: $1 })" replace: "$&,$self.friendsSinceOld({ userId: $1 })"
} }
}, },
// User DMs "User Profile" popup in the right // DM User Sidebar - old layout
{ {
find: ".PROFILE_PANEL,", find: ".PROFILE_PANEL,",
replacement: { replacement: {
match: /,{userId:([^,]+?)}\)/, match: /,{userId:([^,]+?)}\)/,
replace: "$&,$self.friendsSince({ userId: $1 })" replace: "$&,$self.friendsSinceOld({ userId: $1 })"
} }
}, },
// User Profile Modal // User Profile Modal - old layout
{ {
find: ".userInfoSectionHeader,", find: ".userInfoSectionHeader,",
replacement: { replacement: {
match: /(\.Messages\.USER_PROFILE_MEMBER_SINCE.+?userId:(.+?),textClassName:)(\i\.userInfoText)}\)/, match: /(\.Messages\.USER_PROFILE_MEMBER_SINCE.+?userId:(.+?),textClassName:)(\i\.userInfoText)}\)/,
replace: (_, rest, userId, textClassName) => `${rest}!$self.getFriendSince(${userId}) ? ${textClassName} : void 0 }), $self.friendsSince({ userId: ${userId}, textClassName: ${textClassName} })` replace: (_, rest, userId, textClassName) => `${rest}!$self.getFriendSince(${userId}) ? ${textClassName} : void 0 }), $self.friendsSinceOld({ userId: ${userId}, textClassName: ${textClassName} })`
}
},
// DM User Sidebar - new layout
{
find: ".PANEL}),nicknameIcons",
replacement: {
match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id)}\)}\)/,
replace: "$&,$self.friendsSinceNew({userId:$1,isSidebar:true})"
}
},
// User Profile Modal - new layout
{
find: "action:\"PRESS_APP_CONNECTION\"",
replacement: {
match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id),.{0,100}}\)}\),/,
replace: "$&,$self.friendsSinceNew({userId:$1,isSidebar:false}),"
} }
} }
], ],
@ -63,7 +77,7 @@ export default definePlugin({
} }
}, },
friendsSince: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => { friendsSinceOld: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => {
if (!RelationshipStore.isFriend(userId)) return null; if (!RelationshipStore.isFriend(userId)) return null;
const friendsSince = RelationshipStore.getSince(userId); const friendsSince = RelationshipStore.getSince(userId);
@ -71,11 +85,11 @@ export default definePlugin({
return ( return (
<div className={lastSection.section}> <div className={lastSection.section}>
<Heading variant="eyebrow" className={cl("title")}> <Heading variant="eyebrow">
Friends Since Friends Since
</Heading> </Heading>
<div className={container.memberSinceWrapper}> <div className={containerWrapper.memberSinceWrapper}>
{!!getCurrentChannel()?.guild_id && ( {!!getCurrentChannel()?.guild_id && (
<svg <svg
aria-hidden="true" aria-hidden="true"
@ -88,11 +102,55 @@ export default definePlugin({
<path d="M3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z" /> <path d="M3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z" />
</svg> </svg>
)} )}
<Text variant="text-sm/normal" className={classes(cl("body"), textClassName)}> <Text variant="text-sm/normal" className={textClassName}>
{getCreatedAtDate(friendsSince, locale.getLocale())} {getCreatedAtDate(friendsSince, locale.getLocale())}
</Text> </Text>
</div> </div>
</div> </div>
); );
}, { noop: true }) }, { noop: true }),
friendsSinceNew: ErrorBoundary.wrap(({ userId, isSidebar }: { userId: string; isSidebar: boolean; }) => {
if (!RelationshipStore.isFriend(userId)) return null;
const friendsSince = RelationshipStore.getSince(userId);
if (!friendsSince) return null;
return (
<section className={section.section}>
<Heading variant="text-xs/semibold" style={isSidebar ? {} : { color: "var(--header-secondary)" }}>
Friends Since
</Heading>
{
isSidebar ? (
<Text variant="text-sm/normal">
{getCreatedAtDate(friendsSince, locale.getLocale())}
</Text>
) : (
<div className={containerWrapper.memberSinceWrapper}>
<div className={container.memberSince}>
{!!getCurrentChannel()?.guild_id && (
<svg
aria-hidden="true"
width="16"
height="16"
viewBox="0 0 24 24"
fill="var(--interactive-normal)"
>
<path d="M13 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" />
<path d="M3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z" />
</svg>
)}
<Text variant="text-sm/normal">
{getCreatedAtDate(friendsSince, locale.getLocale())}
</Text>
</div>
</div>
)
}
</section>
);
}, { noop: true }),
}); });

View file

@ -1,12 +0,0 @@
/* copy pasted from discord */
.vc-friendssince-title {
display: flex;
font-weight: 700;
margin-bottom: 6px
}
.vc-friendssince-body {
font-size: 14px;
line-height: 18px
}

View file

@ -222,6 +222,13 @@ export default definePlugin({
} }
] ]
}, },
{
find: '="ActivityTrackingStore",',
replacement: {
match: /getVisibleRunningGames\(\).+?;(?=for)(?<=(\i)=\i\.\i\.getVisibleRunningGames.+?)/,
replace: (m, runningGames) => `${m}${runningGames}=${runningGames}.filter(({id,name})=>$self.isActivityNotIgnored({type:0,application_id:id,name}));`
}
},
{ {
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY", find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
replacement: { replacement: {

View file

@ -59,11 +59,15 @@ export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
delete patch.group; delete patch.group;
} }
if (patch.predicate && !patch.predicate()) return;
canonicalizeFind(patch); canonicalizeFind(patch);
if (!Array.isArray(patch.replacement)) { if (!Array.isArray(patch.replacement)) {
patch.replacement = [patch.replacement]; patch.replacement = [patch.replacement];
} }
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());
if (IS_REPORTER) { if (IS_REPORTER) {
patch.replacement.forEach(r => { patch.replacement.forEach(r => {
delete r.predicate; delete r.predicate;

View file

@ -27,12 +27,12 @@ export default definePlugin({
dependencies: ["CommandsAPI"], dependencies: ["CommandsAPI"],
commands: [ commands: [
{ name: "dissatisfaction", description: " " }, { name: "dissatisfaction", description: " " },
{ name: "smug", description: " ಠ_ಠ" }, { name: "smug", description: "ಠ_ಠ" },
{ name: "happy", description: " ヽ(´▽`)/" }, { name: "happy", description: "ヽ(´▽`)/" },
{ name: "crying", description: " ಥ_ಥ" }, { name: "crying", description: "ಥ_ಥ" },
{ name: "angry", description: " ヽ(`Д´)ノ" }, { name: "angry", description: "ヽ(`Д´)ノ" },
{ name: "anger", description: " ヽ(`皿′o)ノ" }, { name: "anger", description: "ヽ(`皿′o)ノ" },
{ name: "joy", description: " <( ̄︶ ̄)>" }, { name: "joy", description: "<( ̄︶ ̄)>" },
{ name: "blush", description: "૮ ˶ᵔ ᵕ ᵔ˶ ა" }, { name: "blush", description: "૮ ˶ᵔ ᵕ ᵔ˶ ა" },
{ name: "confused", description: "(•ิ_•ิ)?" }, { name: "confused", description: "(•ิ_•ิ)?" },
{ name: "sleeping", description: "(ᴗ_ᴗ)" }, { name: "sleeping", description: "(ᴗ_ᴗ)" },
@ -42,7 +42,7 @@ export default definePlugin({
...data, ...data,
options: [OptionalMessageOption], options: [OptionalMessageOption],
execute: opts => ({ execute: opts => ({
content: findOption(opts, "message", "") + data.description content: findOption(opts, "message", "") + " " + data.description
}) })
})) }))
}); });

View file

@ -49,7 +49,7 @@ export default definePlugin({
find: ".Messages.MUTUAL_GUILDS_WITH_END_COUNT", // Note: the module is lazy-loaded find: ".Messages.MUTUAL_GUILDS_WITH_END_COUNT", // Note: the module is lazy-loaded
replacement: { replacement: {
match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/, match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/,
replace: '(arguments[0].user.bot||arguments[0].isCurrentUser)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),' replace: '$self.isBotOrSelf(arguments[0].user)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),'
} }
}, },
{ {
@ -58,9 +58,24 @@ export default definePlugin({
match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/, match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/,
replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs({user: $1, onClose: $2});" replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs({user: $1, onClose: $2});"
} }
},
{
find: ".MUTUAL_FRIENDS?(",
replacement: [
{
match: /(?<=onItemSelect:\i,children:)(\i)\.map/,
replace: "[...$1, ...($self.isBotOrSelf(arguments[0].user) ? [] : [{section:'MUTUAL_GDMS',text:'Mutual Groups'}])].map"
},
{
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
replace: "$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):$&"
}
]
} }
], ],
isBotOrSelf: (user: User) => user.bot || user.id === UserStore.getCurrentUser().id,
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => { renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => ( const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
<Clickable <Clickable

View file

@ -16,10 +16,17 @@
* 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 {
findGroupChildrenByChildId,
NavContextMenuPatchCallback
} from "@api/ContextMenu";
import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { CogWheel } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
import { Menu } from "@webpack/common";
import { Guild } from "discord-types/general";
const { updateGuildNotificationSettings } = findByPropsLazy("updateGuildNotificationSettings"); const { updateGuildNotificationSettings } = findByPropsLazy("updateGuildNotificationSettings");
const { toggleShowAllChannels } = mapMangledModuleLazy(".onboardExistingMember(", { const { toggleShowAllChannels } = mapMangledModuleLazy(".onboardExistingMember(", {
@ -73,48 +80,68 @@ const settings = definePluginSettings({
} }
}); });
const makeContextMenuPatch: (shouldAddIcon: boolean) => NavContextMenuPatchCallback = (shouldAddIcon: boolean) => (children, { guild }: { guild: Guild, onClose(): void; }) => {
if (!guild) return;
const group = findGroupChildrenByChildId("privacy", children);
group?.push(
<Menu.MenuItem
label="Apply NewGuildSettings"
id="vc-newguildsettings-apply"
icon={shouldAddIcon ? CogWheel : void 0}
action={() => applyDefaultSettings(guild.id)}
/>
);
};
function applyDefaultSettings(guildId: string | null) {
if (guildId === "@me" || guildId === "null" || guildId == null) return;
updateGuildNotificationSettings(guildId,
{
muted: settings.store.guild,
suppress_everyone: settings.store.everyone,
suppress_roles: settings.store.role,
mute_scheduled_events: settings.store.events,
notify_highlights: settings.store.highlights ? 1 : 0
});
if (settings.store.messages !== 3) {
updateGuildNotificationSettings(guildId,
{
message_notifications: settings.store.messages,
});
}
if (settings.store.showAllChannels && isOptInEnabledForGuild(guildId)) {
toggleShowAllChannels(guildId);
}
}
migratePluginSettings("NewGuildSettings", "MuteNewGuild"); migratePluginSettings("NewGuildSettings", "MuteNewGuild");
export default definePlugin({ export default definePlugin({
name: "NewGuildSettings", name: "NewGuildSettings",
description: "Automatically mute new servers and change various other settings upon joining", description: "Automatically mute new servers and change various other settings upon joining",
tags: ["MuteNewGuild", "mute", "server"], tags: ["MuteNewGuild", "mute", "server"],
authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi, Devs.GabiRP], authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi, Devs.GabiRP],
contextMenus: {
"guild-context": makeContextMenuPatch(false),
"guild-header-popout": makeContextMenuPatch(true)
},
patches: [ patches: [
{ {
find: ",acceptInvite(", find: ",acceptInvite(",
replacement: { replacement: {
match: /INVITE_ACCEPT_SUCCESS.+?,(\i)=null!==.+?;/, match: /INVITE_ACCEPT_SUCCESS.+?,(\i)=null!==.+?;/,
replace: (m, guildId) => `${m}$self.handleMute(${guildId});` replace: (m, guildId) => `${m}$self.applyDefaultSettings(${guildId});`
} }
}, },
{ {
find: "{joinGuild:", find: "{joinGuild:",
replacement: { replacement: {
match: /guildId:(\i),lurker:(\i).{0,20}}\)\);/, match: /guildId:(\i),lurker:(\i).{0,20}}\)\);/,
replace: (m, guildId, lurker) => `${m}if(!${lurker})$self.handleMute(${guildId});` replace: (m, guildId, lurker) => `${m}if(!${lurker})$self.applyDefaultSettings(${guildId});`
} }
} }
], ],
settings, settings,
applyDefaultSettings
handleMute(guildId: string | null) {
if (guildId === "@me" || guildId === "null" || guildId == null) return;
updateGuildNotificationSettings(guildId,
{
muted: settings.store.guild,
suppress_everyone: settings.store.everyone,
suppress_roles: settings.store.role,
mute_scheduled_events: settings.store.events,
notify_highlights: settings.store.highlights ? 1 : 0
});
if (settings.store.messages !== 3) {
updateGuildNotificationSettings(guildId,
{
message_notifications: settings.store.messages,
});
}
if (settings.store.showAllChannels && isOptInEnabledForGuild(guildId)) {
toggleShowAllChannels(guildId);
}
}
}); });

View file

@ -43,7 +43,7 @@ const Classes = proxyLazyWebpack(() =>
)) ))
) as Record<"roles" | "rolePill" | "rolePillBorder" | "desaturateUserColors" | "flex" | "alignCenter" | "justifyCenter" | "svg" | "background" | "dot" | "dotBorderColor" | "roleCircle" | "dotBorderBase" | "flex" | "alignCenter" | "justifyCenter" | "wrap" | "root" | "role" | "roleRemoveButton" | "roleDot" | "roleFlowerStar" | "roleRemoveIcon" | "roleRemoveIconFocused" | "roleVerifiedIcon" | "roleName" | "roleNameOverflow" | "actionButton" | "overflowButton" | "addButton" | "addButtonIcon" | "overflowRolesPopout" | "overflowRolesPopoutArrowWrapper" | "overflowRolesPopoutArrow" | "popoutBottom" | "popoutTop" | "overflowRolesPopoutHeader" | "overflowRolesPopoutHeaderIcon" | "overflowRolesPopoutHeaderText" | "roleIcon", string>; ) as Record<"roles" | "rolePill" | "rolePillBorder" | "desaturateUserColors" | "flex" | "alignCenter" | "justifyCenter" | "svg" | "background" | "dot" | "dotBorderColor" | "roleCircle" | "dotBorderBase" | "flex" | "alignCenter" | "justifyCenter" | "wrap" | "root" | "role" | "roleRemoveButton" | "roleDot" | "roleFlowerStar" | "roleRemoveIcon" | "roleRemoveIconFocused" | "roleVerifiedIcon" | "roleName" | "roleNameOverflow" | "actionButton" | "overflowButton" | "addButton" | "addButtonIcon" | "overflowRolesPopout" | "overflowRolesPopoutArrowWrapper" | "overflowRolesPopoutArrow" | "popoutBottom" | "popoutTop" | "overflowRolesPopoutHeader" | "overflowRolesPopoutHeaderIcon" | "overflowRolesPopoutHeaderText" | "roleIcon", string>;
function UserPermissionsComponent({ guild, guildMember, showBorder }: { guild: Guild; guildMember: GuildMember; showBorder: boolean; }) { function UserPermissionsComponent({ guild, guildMember, showBorder, forceOpen = false }: { guild: Guild; guildMember: GuildMember; showBorder: boolean; forceOpen?: boolean; }) {
const stns = settings.use(["permissionsSortOrder"]); const stns = settings.use(["permissionsSortOrder"]);
const [rolePermissions, userPermissions] = useMemo(() => { const [rolePermissions, userPermissions] = useMemo(() => {
@ -95,6 +95,7 @@ function UserPermissionsComponent({ guild, guildMember, showBorder }: { guild: G
return ( return (
<ExpandableHeader <ExpandableHeader
forceOpen={forceOpen}
headerText="Permissions" headerText="Permissions"
moreTooltipText="Role Details" moreTooltipText="Role Details"
onMoreClick={() => onMoreClick={() =>

View file

@ -20,15 +20,22 @@ import "./styles.css";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { SafetyIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { ChannelStore, GuildMemberStore, GuildStore, Menu, PermissionsBits, UserStore } from "@webpack/common"; import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, Menu, PermissionsBits, Popout, TooltipContainer, UserStore } from "@webpack/common";
import type { Guild, GuildMember } from "discord-types/general"; import type { Guild, GuildMember } from "discord-types/general";
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions"; import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions";
import UserPermissions from "./components/UserPermissions"; import UserPermissions from "./components/UserPermissions";
import { getSortedRoles, sortPermissionOverwrites } from "./utils"; import { getSortedRoles, sortPermissionOverwrites } from "./utils";
const PopoutClasses = findByPropsLazy("container", "scroller", "list");
const RoleButtonClasses = findByPropsLazy("button", "buttonInner", "icon", "text");
export const enum PermissionsSortOrder { export const enum PermissionsSortOrder {
HighestRole, HighestRole,
LowestRole LowestRole
@ -168,10 +175,45 @@ export default definePlugin({
match: /showBorder:(.{0,60})}\),(?<=guild:(\i),guildMember:(\i),.+?)/, match: /showBorder:(.{0,60})}\),(?<=guild:(\i),guildMember:(\i),.+?)/,
replace: (m, showBoder, guild, guildMember) => `${m}$self.UserPermissions(${guild},${guildMember},${showBoder}),` replace: (m, showBoder, guild, guildMember) => `${m}$self.UserPermissions(${guild},${guildMember},${showBoder}),`
} }
},
{
find: ".VIEW_ALL_ROLES,",
replacement: {
match: /children:"\+"\.concat\(\i\.length-\i\.length\).{0,20}\}\),/,
replace: "$&$self.ViewPermissionsButton(arguments[0]),"
}
} }
], ],
UserPermissions: (guild: Guild, guildMember: GuildMember | undefined, showBoder: boolean) => !!guildMember && <UserPermissions guild={guild} guildMember={guildMember} showBorder={showBoder} />, UserPermissions: (guild: Guild, guildMember: GuildMember | undefined, showBorder: boolean) =>
!!guildMember && <UserPermissions guild={guild} guildMember={guildMember} showBorder={showBorder} />,
ViewPermissionsButton: ErrorBoundary.wrap(({ guild, guildMember }: { guild: Guild; guildMember: GuildMember; }) => (
<Popout
position="bottom"
align="center"
renderPopout={() => (
<Dialog className={PopoutClasses.container} style={{ width: "500px" }}>
<UserPermissions guild={guild} guildMember={guildMember} showBorder forceOpen />
</Dialog>
)}
>
{popoutProps => (
<TooltipContainer text="View Permissions">
<Button
{...popoutProps}
color={Button.Colors.CUSTOM}
look={Button.Looks.FILLED}
size={Button.Sizes.NONE}
innerClassName={classes(RoleButtonClasses.buttonInner, RoleButtonClasses.icon)}
className={classes(RoleButtonClasses.button, RoleButtonClasses.icon, "vc-permviewer-role-button")}
>
<SafetyIcon height="16" width="16" />
</Button>
</TooltipContainer>
)}
</Popout>
), { noop: true }),
contextMenus: { contextMenus: {
"user-context": makeContextMenuPatch("roles", MenuItemParentType.User), "user-context": makeContextMenuPatch("roles", MenuItemParentType.User),

View file

@ -149,3 +149,21 @@
.vc-permviewer-perms-perms-item .vc-info-icon:hover { .vc-permviewer-perms-perms-item .vc-info-icon:hover {
color: var(--interactive-active); color: var(--interactive-active);
} }
/* copy pasted from discord cause impossible to webpack find */
.vc-permviewer-role-button {
border-radius: var(--radius-xs);
background: var(--bg-mod-faint);
color: var(--interactive-normal);
border: 1px solid var(--border-faint);
/* stylelint-disable-next-line value-no-vendor-prefix */
width: -moz-fit-content;
width: fit-content;
height: 24px;
padding: 4px
}
.custom-profile-theme .vc-permviewer-role-button {
background: rgb(var(--bg-overlay-color)/var(--bg-overlay-opacity-6));
border-color: var(--profile-body-border-color)
}

View file

@ -16,7 +16,9 @@
* 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 { addBadge, BadgePosition, ProfileBadge, removeBadge } from "@api/Badges"; import "./style.css";
import { addBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeBadge } from "@api/Badges";
import { addDecorator, removeDecorator } from "@api/MemberListDecorators"; import { addDecorator, removeDecorator } from "@api/MemberListDecorators";
import { addDecoration, removeDecoration } from "@api/MessageDecorations"; import { addDecoration, removeDecoration } from "@api/MessageDecorations";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
@ -27,7 +29,20 @@ import { findByPropsLazy, findStoreLazy } from "@webpack";
import { PresenceStore, Tooltip, UserStore } from "@webpack/common"; import { PresenceStore, Tooltip, UserStore } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
const SessionsStore = findStoreLazy("SessionsStore"); export interface Session {
sessionId: string;
status: string;
active: boolean;
clientInfo: {
version: number;
os: string;
client: string;
};
}
const SessionsStore = findStoreLazy("SessionsStore") as {
getSessions(): Record<string, Session>;
};
function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) { function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) {
return ({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) => ( return ({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) => (
@ -67,15 +82,11 @@ const PlatformIcon = ({ platform, status, small }: { platform: Platform, status:
return <Icon color={StatusUtils.useStatusFillColor(status)} tooltip={tooltip} small={small} />; return <Icon color={StatusUtils.useStatusFillColor(status)} tooltip={tooltip} small={small} />;
}; };
const getStatus = (id: string): Record<Platform, string> => PresenceStore.getState()?.clientStatuses?.[id]; function ensureOwnStatus(user: User) {
const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, small = false }: { user: User; wantMargin?: boolean; wantTopMargin?: boolean; small?: boolean; }) => {
if (!user || user.bot) return null;
if (user.id === UserStore.getCurrentUser().id) { if (user.id === UserStore.getCurrentUser().id) {
const sessions = SessionsStore.getSessions(); const sessions = SessionsStore.getSessions();
if (typeof sessions !== "object") return null; if (typeof sessions !== "object") return null;
const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => { const sortedSessions = Object.values(sessions).sort(({ status: a }, { status: b }) => {
if (a === b) return 0; if (a === b) return 0;
if (a === "online") return 1; if (a === "online") return 1;
if (b === "online") return -1; if (b === "online") return -1;
@ -84,7 +95,7 @@ const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, sma
return 0; return 0;
}); });
const ownStatus = Object.values(sortedSessions).reduce((acc: any, curr: any) => { const ownStatus = Object.values(sortedSessions).reduce((acc, curr) => {
if (curr.clientInfo.client !== "unknown") if (curr.clientInfo.client !== "unknown")
acc[curr.clientInfo.client] = curr.status; acc[curr.clientInfo.client] = curr.status;
return acc; return acc;
@ -93,6 +104,37 @@ const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, sma
const { clientStatuses } = PresenceStore.getState(); const { clientStatuses } = PresenceStore.getState();
clientStatuses[UserStore.getCurrentUser().id] = ownStatus; clientStatuses[UserStore.getCurrentUser().id] = ownStatus;
} }
}
function getBadges({ userId }: BadgeUserArgs): ProfileBadge[] {
const user = UserStore.getUser(userId);
if (!user || user.bot) return [];
ensureOwnStatus(user);
const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record<Platform, string>;
if (!status) return [];
return Object.entries(status).map(([platform, status]) => ({
component: () => (
<span className="vc-platform-indicator">
<PlatformIcon
key={platform}
platform={platform as Platform}
status={status}
small={false}
/>
</span>
),
key: `vc-platform-indicator-${platform}`
}));
}
const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, small = false }: { user: User; wantMargin?: boolean; wantTopMargin?: boolean; small?: boolean; }) => {
if (!user || user.bot) return null;
ensureOwnStatus(user);
const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record<Platform, string>; const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record<Platform, string>;
if (!status) return null; if (!status) return null;
@ -112,17 +154,10 @@ const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, sma
<span <span
className="vc-platform-indicator" className="vc-platform-indicator"
style={{ style={{
display: "inline-flex",
justifyContent: "center",
alignItems: "center",
marginLeft: wantMargin ? 4 : 0, marginLeft: wantMargin ? 4 : 0,
verticalAlign: "top",
position: "relative",
top: wantTopMargin ? 2 : 0, top: wantTopMargin ? 2 : 0,
padding: !wantMargin ? 1 : 0,
gap: 2 gap: 2
}} }}
> >
{icons} {icons}
</span> </span>
@ -130,10 +165,8 @@ const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, sma
}; };
const badge: ProfileBadge = { const badge: ProfileBadge = {
component: p => <PlatformIndicator {...p} user={UserStore.getUser(p.userId)} wantMargin={false} />, getBadges,
position: BadgePosition.START, position: BadgePosition.START,
shouldShow: userInfo => !!Object.keys(getStatus(userInfo.userId) ?? {}).length,
key: "indicator"
}; };
const indicatorLocations = { const indicatorLocations = {

View file

@ -0,0 +1,7 @@
.vc-platform-indicator {
display: inline-flex;
justify-content: center;
align-items: center;
vertical-align: top;
position: relative;
}

View file

@ -13,13 +13,12 @@ import { Flex, Menu } from "@webpack/common";
const DefaultEngines = { const DefaultEngines = {
Google: "https://www.google.com/search?q=", Google: "https://www.google.com/search?q=",
DuckDuckGo: "https://duckduckgo.com/", DuckDuckGo: "https://duckduckgo.com/",
Brave: "https://search.brave.com/search?q=",
Bing: "https://www.bing.com/search?q=", Bing: "https://www.bing.com/search?q=",
Yahoo: "https://search.yahoo.com/search?p=", Yahoo: "https://search.yahoo.com/search?p=",
GitHub: "https://github.com/search?q=",
Kagi: "https://kagi.com/search?q=",
Yandex: "https://yandex.com/search/?text=", Yandex: "https://yandex.com/search/?text=",
AOL: "https://search.aol.com/aol/search?q=", GitHub: "https://github.com/search?q=",
Baidu: "https://www.baidu.com/s?wd=", Reddit: "https://www.reddit.com/search?q=",
Wikipedia: "https://wikipedia.org/w/index.php?search=", Wikipedia: "https://wikipedia.org/w/index.php?search=",
} as const; } as const;
@ -55,7 +54,7 @@ function makeSearchItem(src: string) {
key="search-text" key="search-text"
id="vc-search-text" id="vc-search-text"
> >
{Object.keys(Engines).map((engine, i) => { {Object.keys(Engines).map(engine => {
const key = "vc-search-content-" + engine; const key = "vc-search-content-" + engine;
return ( return (
<Menu.MenuItem <Menu.MenuItem
@ -70,7 +69,7 @@ function makeSearchItem(src: string) {
aria-hidden="true" aria-hidden="true"
height={16} height={16}
width={16} width={16}
src={`https://www.google.com/s2/favicons?domain=${Engines[engine]}`} src={`https://www.google.com/s2/favicons?domain=${Engines[engine]}&sz=64`}
/> />
{engine} {engine}
</Flex> </Flex>

View file

@ -27,7 +27,7 @@ import { cl } from "../utils";
import ReviewComponent from "./ReviewComponent"; import ReviewComponent from "./ReviewComponent";
import ReviewsView, { ReviewsInputComponent } from "./ReviewsView"; import ReviewsView, { ReviewsInputComponent } from "./ReviewsView";
function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: string; name: string; }) { function Modal({ modalProps, modalKey, discordId, name }: { modalProps: any; modalKey: string, discordId: string; name: string; }) {
const [data, setData] = useState<Response>(); const [data, setData] = useState<Response>();
const [signal, refetch] = useForceUpdater(true); const [signal, refetch] = useForceUpdater(true);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@ -76,6 +76,7 @@ function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: st
discordId={discordId} discordId={discordId}
name={name} name={name}
refetch={refetch} refetch={refetch}
modalKey={modalKey}
/> />
{!!reviewCount && ( {!!reviewCount && (
@ -95,11 +96,14 @@ function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: st
} }
export function openReviewsModal(discordId: string, name: string) { export function openReviewsModal(discordId: string, name: string) {
const modalKey = "vc-rdb-modal-" + Date.now();
openModal(props => ( openModal(props => (
<Modal <Modal
modalKey={modalKey}
modalProps={props} modalProps={props}
discordId={discordId} discordId={discordId}
name={name} name={name}
/> />
)); ), { modalKey });
} }

View file

@ -119,7 +119,9 @@ function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch():
} }
export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) { export function ReviewsInputComponent(
{ discordId, isAuthor, refetch, name, modalKey }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; modalKey?: string; }
) {
const { token } = Auth; const { token } = Auth;
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const inputType = ChatInputTypes.FORM; const inputType = ChatInputTypes.FORM;
@ -148,6 +150,7 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: {
type={inputType} type={inputType}
disableThemedBackground={true} disableThemedBackground={true}
setEditorRef={ref => editorRef.current = ref} setEditorRef={ref => editorRef.current = ref}
parentModalKey={modalKey}
textValue="" textValue=""
onSubmit={ onSubmit={
async res => { async res => {

View file

@ -21,10 +21,12 @@ import "./style.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { ExpandableHeader } from "@components/ExpandableHeader"; import { ExpandableHeader } from "@components/ExpandableHeader";
import { OpenExternalIcon } from "@components/Icons"; import { NotesIcon, OpenExternalIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { classes } from "@utils/misc";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Alerts, Menu, Parser, useState } from "@webpack/common"; import { findByPropsLazy } from "@webpack";
import { Alerts, Button, Menu, Parser, TooltipContainer, useState } from "@webpack/common";
import { Guild, User } from "discord-types/general"; import { Guild, User } from "discord-types/general";
import { Auth, initAuth, updateAuth } from "./auth"; import { Auth, initAuth, updateAuth } from "./auth";
@ -35,6 +37,9 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
import { settings } from "./settings"; import { settings } from "./settings";
import { showToast } from "./utils"; import { showToast } from "./utils";
const PopoutClasses = findByPropsLazy("container", "scroller", "list");
const RoleButtonClasses = findByPropsLazy("button", "buttonInner", "icon", "text");
const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => { const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => {
if (!guild) return; if (!guild) return;
children.push( children.push(
@ -69,7 +74,8 @@ export default definePlugin({
"guild-header-popout": guildPopoutPatch, "guild-header-popout": guildPopoutPatch,
"guild-context": guildPopoutPatch, "guild-context": guildPopoutPatch,
"user-context": userContextPatch, "user-context": userContextPatch,
"user-profile-actions": userContextPatch "user-profile-actions": userContextPatch,
"user-profile-overflow-menu": userContextPatch
}, },
patches: [ patches: [
@ -79,6 +85,13 @@ export default definePlugin({
match: /user:(\i),setNote:\i,canDM.+?\}\)/, match: /user:(\i),setNote:\i,canDM.+?\}\)/,
replace: "$&,$self.getReviewsComponent($1)" replace: "$&,$self.getReviewsComponent($1)"
} }
},
{
find: ".BITE_SIZE,user:",
replacement: {
match: /(?<=\.BITE_SIZE,children:\[)\(0,\i\.jsx\)\(\i\.\i,\{user:(\i),/,
replace: "$self.BiteSizeReviewsButton({user:$1}),$&"
}
} }
], ],
@ -159,5 +172,22 @@ export default definePlugin({
/> />
</ExpandableHeader> </ExpandableHeader>
); );
}, { message: "Failed to render Reviews" }) }, { message: "Failed to render Reviews" }),
BiteSizeReviewsButton: ErrorBoundary.wrap(({ user }: { user: User; }) => {
return (
<TooltipContainer text="View Reviews">
<Button
onClick={() => openReviewsModal(user.id, user.username)}
look={Button.Looks.FILLED}
size={Button.Sizes.NONE}
color={RoleButtonClasses.color}
className={classes(RoleButtonClasses.button, RoleButtonClasses.banner)}
innerClassName={classes(RoleButtonClasses.buttonInner, RoleButtonClasses.banner)}
>
<NotesIcon height={16} width={16} />
</Button>
</TooltipContainer>
);
}, { noop: true })
}); });

View file

@ -0,0 +1,6 @@
# ShowAllRoles
Display all roles on the new profiles instead of limiting them to the default two rows.
![image](https://github.com/Vendicated/Vencord/assets/71079641/3f021f03-c6f9-4fe5-83ac-a1891b5e4b37)

View file

@ -0,0 +1,23 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "ShowAllRoles",
description: "Show all roles in new profiles.",
authors: [Devs.Luna],
patches: [
{
find: ".Messages.VIEW_ALL_ROLES",
replacement: {
match: /return null!=\i(?=\?\i\.slice)/,
replace: "return false"
}
}
]
});

View file

@ -211,7 +211,7 @@ export default definePlugin({
} }
}, },
{ {
find: ".BITE_SIZE,onOpenProfile", find: /\.BITE_SIZE,onOpenProfile:\i,usernameIcon:/,
replacement: { replacement: {
match: /currentUser:\i,guild:\i,onOpenProfile:.+?}\)(?=])(?<=user:(\i),bio:null==(\i)\?.+?)/, match: /currentUser:\i,guild:\i,onOpenProfile:.+?}\)(?=])(?<=user:(\i),bio:null==(\i)\?.+?)/,
replace: "$&,$self.profilePopoutComponent({ user: $1, displayProfile: $2, simplified: true })" replace: "$&,$self.profilePopoutComponent({ user: $1, displayProfile: $2, simplified: true })"

View file

@ -477,12 +477,17 @@ export default definePlugin({
], ],
isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) { isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) {
if (!channel) return false; try {
if (!channel) return false;
if (channel.channelId) channel = ChannelStore.getChannel(channel.channelId); if (channel.channelId) channel = ChannelStore.getChannel(channel.channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false; if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false;
return !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) || checkConnect && !PermissionStore.can(PermissionsBits.CONNECT, channel); return !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) || checkConnect && !PermissionStore.can(PermissionsBits.CONNECT, channel);
} catch (e) {
console.error("[ViewHiddenChannels#isHiddenChannel]: ", e);
return false;
}
}, },
resolveGuildChannels(channels: Record<string | number, Array<{ channel: Channel; comparator: number; }> | string | number>, shouldIncludeHidden: boolean) { resolveGuildChannels(channels: Record<string | number, Array<{ channel: Channel; comparator: number; }> | string | number>, shouldIncludeHidden: boolean) {

View file

@ -105,6 +105,15 @@ export default definePlugin({
replace: "=[]" replace: "=[]"
} }
}, },
// empty 2nd word filter
{
find: '"pepe","nude"',
predicate: () => settings.store.disableDisallowedDiscoveryFilters,
replacement: {
match: /(?<=[?=])\["pepe",.+?\]/,
replace: "[]",
},
},
// patch request that queries if term is allowed // patch request that queries if term is allowed
{ {
find: ".GUILD_DISCOVERY_VALID_TERM", find: ".GUILD_DISCOVERY_VALID_TERM",

View file

@ -66,12 +66,16 @@ export default definePlugin({
const { nick } = author; const { nick } = author;
const prefix = withMentionPrefix ? "@" : ""; const prefix = withMentionPrefix ? "@" : "";
if (username === nick || isRepliedMessage && !settings.store.inReplies)
if (isRepliedMessage && !settings.store.inReplies || username === nick.toLowerCase())
return <>{prefix}{nick}</>; return <>{prefix}{nick}</>;
if (settings.store.mode === "user-nick") if (settings.store.mode === "user-nick")
return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>; return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>;
if (settings.store.mode === "nick-user") if (settings.store.mode === "nick-user")
return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>; return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>;
return <>{prefix}{username}</>; return <>{prefix}{username}</>;
} catch { } catch {
return <>{author?.nick}</>; return <>{author?.nick}</>;

View file

@ -74,9 +74,9 @@ const SilentMessageToggle: ChatBarButton = ({ isMainChat }) => {
viewBox="0 0 24 24" viewBox="0 0 24 24"
style={{ scale: "1.2" }} style={{ scale: "1.2" }}
> >
<path fill="currentColor" mask="url(#_)" d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4c0-.30736.0198-.6101.0582-.907C12.7147 3.03189 12.3611 3 12 3 8.686 3 6 5.686 6 9v5c0 1.657-1.344 3-3 3v1h18v-1c-1.656 0-3-1.343-3-3v-3.2899ZM8.55493 19c.693 1.19 1.96897 2 3.44497 2s2.752-.81 3.445-2H8.55493ZM18.2624 5.50209 21 2.5V1h-4.9651v1.49791h2.4411L16 5.61088V7h5V5.50209h-2.7376Z" /> <path fill="currentColor" mask="url(#vc-silent-msg-mask)" d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4c0-.30736.0198-.6101.0582-.907C12.7147 3.03189 12.3611 3 12 3 8.686 3 6 5.686 6 9v5c0 1.657-1.344 3-3 3v1h18v-1c-1.656 0-3-1.343-3-3v-3.2899ZM8.55493 19c.693 1.19 1.96897 2 3.44497 2s2.752-.81 3.445-2H8.55493ZM18.2624 5.50209 21 2.5V1h-4.9651v1.49791h2.4411L16 5.61088V7h5V5.50209h-2.7376Z" />
{!enabled && <> {!enabled && <>
<mask id="_"> <mask id="vc-silent-msg-mask">
<path fill="#fff" d="M0 0h24v24H0Z" /> <path fill="#fff" d="M0 0h24v24H0Z" />
<path stroke="#000" stroke-width="5.99068" d="M0 24 24 0" /> <path stroke="#000" stroke-width="5.99068" d="M0 24 24 0" />
</mask> </mask>

View file

@ -77,7 +77,7 @@ const settings = definePluginSettings({
}); });
function stringToRegex(str: string) { function stringToRegex(str: string) {
const match = str.match(/^(\/)?(.+?)(?:\/([gimsuy]*))?$/); // Regex to match regex const match = str.match(/^(\/)?(.+?)(?:\/([gimsuyv]*))?$/); // Regex to match regex
return match return match
? new RegExp( ? new RegExp(
match[2], // Pattern match[2], // Pattern

View file

@ -98,8 +98,8 @@ export default definePlugin({
{ {
find: ".popularApplicationCommandIds,", find: ".popularApplicationCommandIds,",
replacement: { replacement: {
match: /applicationId:\i\.id}\),(?=.{0,50}setNote:\i)/, match: /(?<=,)(?=!\i&&!\i&&.{0,50}setNote:)/,
replace: "$&$self.patchPopout(arguments[0]),", replace: "$self.patchPopout(arguments[0]),",
} }
}, },
// below username // below username

View file

@ -200,6 +200,34 @@ export default definePlugin({
match: /supports\(\i\)\{switch\(\i\)\{(case (\i).\i)/, match: /supports\(\i\)\{switch\(\i\)\{(case (\i).\i)/,
replace: "$&.DISABLE_VIDEO:return true;$1" replace: "$&.DISABLE_VIDEO:return true;$1"
} }
},
{
find: ".Messages.SEARCH_WITH_GOOGLE",
replacement: {
match: /\i\.isPlatformEmbedded/,
replace: "true"
}
},
{
find: ".Messages.COPY,hint:",
replacement: [
{
match: /\i\.isPlatformEmbedded/,
replace: "true"
},
{
match: /\i\.\i\.copy/,
replace: "Vencord.Webpack.Common.Clipboard.copy"
}]
},
// Automod add filter words
{
find: '("interactionUsernameProfile',
replacement:
{
match: /\i\.isPlatformEmbedded(?=.{0,50}\.tagName)/,
replace: "true"
},
} }
], ],

View file

@ -529,6 +529,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
nekohaxx: { nekohaxx: {
name: "nekohaxx", name: "nekohaxx",
id: 1176270221628153886n id: 1176270221628153886n
},
Antti: {
name: "Antti",
id: 312974985876471810n
} }
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);

View file

@ -24,7 +24,7 @@ import { DevsById } from "./constants";
* Calls .join(" ") on the arguments * Calls .join(" ") on the arguments
* classes("one", "two") => "one two" * classes("one", "two") => "one two"
*/ */
export function classes(...classes: Array<string | null | undefined>) { export function classes(...classes: Array<string | null | undefined | false>) {
return classes.filter(Boolean).join(" "); return classes.filter(Boolean).join(" ");
} }

View file

@ -8,7 +8,12 @@ export let EXTENSION_BASE_URL: string;
export let EXTENSION_VERSION: string; export let EXTENSION_VERSION: string;
if (IS_EXTENSION) { if (IS_EXTENSION) {
const script = document.querySelector("#vencord-script") as HTMLScriptElement; const listener = (e: MessageEvent) => {
EXTENSION_BASE_URL = script.dataset.extensionBaseUrl!; if (e.data?.type === "vencord:meta") {
EXTENSION_VERSION = script.dataset.version!; ({ EXTENSION_BASE_URL, EXTENSION_VERSION } = e.data.meta);
window.removeEventListener("message", listener);
}
};
window.addEventListener("message", listener);
} }

View file

@ -33,6 +33,7 @@ export let Card: t.Card;
export let Button: t.Button; export let Button: t.Button;
export let Switch: t.Switch; export let Switch: t.Switch;
export let Tooltip: t.Tooltip; export let Tooltip: t.Tooltip;
export let TooltipContainer: t.TooltipContainer;
export let TextInput: t.TextInput; export let TextInput: t.TextInput;
export let TextArea: t.TextArea; export let TextArea: t.TextArea;
export let Text: t.Text; export let Text: t.Text;
@ -66,6 +67,7 @@ waitFor(["FormItem", "Button"], m => {
Button, Button,
FormSwitch: Switch, FormSwitch: Switch,
Tooltip, Tooltip,
TooltipContainer,
TextInput, TextInput,
TextArea, TextArea,
Text, Text,

View file

@ -101,6 +101,28 @@ export type Tooltip = ComponentType<{
export type TooltipPositions = Record<"BOTTOM" | "CENTER" | "LEFT" | "RIGHT" | "TOP" | "WINDOW_CENTER", string>; export type TooltipPositions = Record<"BOTTOM" | "CENTER" | "LEFT" | "RIGHT" | "TOP" | "WINDOW_CENTER", string>;
export type TooltipContainer = ComponentType<PropsWithChildren<{
text: ReactNode;
element?: "div" | "span";
"aria-label"?: string | false;
delay?: number;
/** Tooltip.Colors.BLACK */
color?: string;
/** TooltipPositions.TOP */
position?: string;
spacing?: number;
className?: string;
tooltipClassName?: string | null;
tooltipContentClassName?: string | null;
allowOverflow?: boolean;
forceOpen?: boolean;
hideOnClick?: boolean;
disableTooltipPointerEvents?: boolean;
}>>;
export type Card = ComponentType<PropsWithChildren<HTMLProps<HTMLDivElement> & { export type Card = ComponentType<PropsWithChildren<HTMLProps<HTMLDivElement> & {
editable?: boolean; editable?: boolean;
outline?: boolean; outline?: boolean;
@ -110,6 +132,26 @@ export type Card = ComponentType<PropsWithChildren<HTMLProps<HTMLDivElement> & {
Types: Record<"BRAND" | "CUSTOM" | "DANGER" | "PRIMARY" | "SUCCESS" | "WARNING", string>; Types: Record<"BRAND" | "CUSTOM" | "DANGER" | "PRIMARY" | "SUCCESS" | "WARNING", string>;
}; };
export type ComboboxPopout = ComponentType<PropsWithChildren<{
value: Set<any>;
placeholder: string;
children(query: string): ReactNode[];
onChange(value: any): void;
itemToString?: (item: any) => string;
onClose?(): void;
className?: string;
listClassName?: string;
autoFocus?: boolean;
multiSelect?: boolean;
maxVisibleItems?: number;
showScrollbar?: boolean;
}>>;
export type Button = ComponentType<PropsWithChildren<Omit<HTMLProps<HTMLButtonElement>, "size"> & { export type Button = ComponentType<PropsWithChildren<Omit<HTMLProps<HTMLButtonElement>, "size"> & {
/** Button.Looks.FILLED */ /** Button.Looks.FILLED */
look?: string; look?: string;
@ -375,7 +417,7 @@ export type Popout = ComponentType<{
Animation: typeof PopoutAnimation; Animation: typeof PopoutAnimation;
}; };
export type Dialog = ComponentType<PropsWithChildren<any>>; export type Dialog = ComponentType<JSX.IntrinsicElements["div"]>;
type Resolve = (data: { theme: "light" | "dark", saturation: number; }) => { type Resolve = (data: { theme: "light" | "dark", saturation: number; }) => {
hex(): string; hex(): string;

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 { Guild, GuildMember } from "discord-types/general"; import { Guild, GuildMember, User } from "discord-types/general";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { LiteralUnion } from "type-fest"; import { LiteralUnion } from "type-fest";
@ -256,3 +256,63 @@ export interface PopoutActions {
close(key: string): void; close(key: string): void;
setAlwaysOnTop(key: string, alwaysOnTop: boolean): void; setAlwaysOnTop(key: string, alwaysOnTop: boolean): void;
} }
export type UserNameUtilsTagInclude = LiteralUnion<"auto" | "always" | "never", string>;
export interface UserNameUtilsTagOptions {
forcePomelo?: boolean;
identifiable?: UserNameUtilsTagInclude;
decoration?: UserNameUtilsTagInclude;
mode?: "full" | "username";
}
export interface UsernameUtils {
getGlobalName(user: User): string;
getFormattedName(user: User, useTagInsteadOfUsername?: boolean): string;
getName(user: User): string;
useName(user: User): string;
getUserTag(user: User, options?: UserNameUtilsTagOptions): string;
useUserTag(user: User, options?: UserNameUtilsTagOptions): string;
useDirectMessageRecipient: any;
humanizeStatus: any;
}
export class DisplayProfile {
userId: string;
banner?: string;
bio?: string;
pronouns?: string;
accentColor?: number;
themeColors?: number[];
popoutAnimationParticleType?: any;
profileEffectId?: string;
_userProfile?: any;
_guildMemberProfile?: any;
canUsePremiumProfileCustomization: boolean;
canEditThemes: boolean;
premiumGuildSince: Date | null;
premiumSince: Date | null;
premiumType?: number;
primaryColor?: number;
getBadges(): Array<{
id: string;
description: string;
icon: string;
link?: string;
}>;
getBannerURL(options: { canAnimate: boolean; size: number; }): string;
getLegacyUsername(): string | null;
hasFullProfile(): boolean;
hasPremiumCustomization(): boolean;
hasThemeColors(): boolean;
isUsingGuildMemberBanner(): boolean;
isUsingGuildMemberBio(): boolean;
isUsingGuildMemberPronouns(): boolean;
}
export interface DisplayProfileUtils {
getDisplayProfile(userId: string, guildId?: string, customStores?: any): DisplayProfile | null;
useDisplayProfile(userId: string, guildId?: string, customStores?: any): DisplayProfile | null;
}

View file

@ -73,6 +73,25 @@ const ToastPosition = {
BOTTOM: 1 BOTTOM: 1
}; };
export interface ToastData {
message: string,
id: string,
/**
* Toasts.Type
*/
type: number,
options?: ToastOptions;
}
export interface ToastOptions {
/**
* Toasts.Position
*/
position?: number;
component?: React.ReactNode,
duration?: number;
}
export const Toasts = { export const Toasts = {
Type: ToastType, Type: ToastType,
Position: ToastPosition, Position: ToastPosition,
@ -81,23 +100,9 @@ export const Toasts = {
// hack to merge with the following interface, dunno if there's a better way // hack to merge with the following interface, dunno if there's a better way
...{} as { ...{} as {
show(data: { show(data: ToastData): void;
message: string,
id: string,
/**
* Toasts.Type
*/
type: number,
options?: {
/**
* Toasts.Position
*/
position?: number;
component?: React.ReactNode,
duration?: number;
};
}): void;
pop(): void; pop(): void;
create(message: string, type: number, options?: ToastOptions): ToastData;
} }
}; };
@ -105,18 +110,15 @@ export const Toasts = {
waitFor("showToast", m => { waitFor("showToast", m => {
Toasts.show = m.showToast; Toasts.show = m.showToast;
Toasts.pop = m.popToast; Toasts.pop = m.popToast;
Toasts.create = m.createToast;
}); });
/** /**
* Show a simple toast. If you need more options, use Toasts.show manually * Show a simple toast. If you need more options, use Toasts.show manually
*/ */
export function showToast(message: string, type = ToastType.MESSAGE) { export function showToast(message: string, type = ToastType.MESSAGE, options?: ToastOptions) {
Toasts.show({ Toasts.show(Toasts.create(message, type, options));
id: Toasts.genId(),
message,
type
});
} }
export const UserUtils = { export const UserUtils = {
@ -172,3 +174,9 @@ export const PopoutActions: t.PopoutActions = mapMangledModuleLazy('type:"POPOUT
close: filters.byCode('type:"POPOUT_WINDOW_CLOSE"'), close: filters.byCode('type:"POPOUT_WINDOW_CLOSE"'),
setAlwaysOnTop: filters.byCode('type:"POPOUT_WINDOW_SET_ALWAYS_ON_TOP"'), setAlwaysOnTop: filters.byCode('type:"POPOUT_WINDOW_SET_ALWAYS_ON_TOP"'),
}); });
export const UsernameUtils: t.UsernameUtils = findByPropsLazy("useName", "getGlobalName");
export const DisplayProfileUtils: t.DisplayProfileUtils = mapMangledModuleLazy(/=\i\.getUserProfile\(\i\),\i=\i\.getGuildMemberProfile\(/, {
getDisplayProfile: filters.byCode(".getGuildMemberProfile("),
useDisplayProfile: filters.byCode(/\[\i\.\i,\i\.\i],\(\)=>/)
});

View file

@ -18,7 +18,7 @@
import { WEBPACK_CHUNK } from "@utils/constants"; import { WEBPACK_CHUNK } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches"; import { canonicalizeReplacement } from "@utils/patches";
import { PatchReplacement } from "@utils/types"; import { PatchReplacement } from "@utils/types";
import { WebpackInstance } from "discord-types/other"; import { WebpackInstance } from "discord-types/other";
@ -27,7 +27,6 @@ import { patches } from "../plugins";
import { _initWebpack, beforeInitListeners, factoryListeners, moduleListeners, subscriptions, wreq } from "."; import { _initWebpack, beforeInitListeners, factoryListeners, moduleListeners, subscriptions, wreq } from ".";
const logger = new Logger("WebpackInterceptor", "#8caaee"); const logger = new Logger("WebpackInterceptor", "#8caaee");
const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/);
let webpackChunk: any[]; let webpackChunk: any[];
@ -53,94 +52,60 @@ Object.defineProperty(window, WEBPACK_CHUNK, {
} }
}); });
// wreq.O is the webpack onChunksLoaded function
// Discord uses it to await for all the chunks to be loaded before initializing the app
// We monkey patch it to also monkey patch the initialize app callback to get immediate access to the webpack require and run our listeners before doing it
Object.defineProperty(Function.prototype, "O", {
configurable: true,
set(onChunksLoaded: any) {
// When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here.
// This ensures we actually got the right one
// this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it
const { stack } = new Error();
if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && String(this.e).includes("Promise.all")) {
logger.info("Found main WebpackRequire.onChunksLoaded");
delete (Function.prototype as any).O;
const originalOnChunksLoaded = onChunksLoaded;
onChunksLoaded = function (this: unknown, result: any, chunkIds: string[], callback: () => any, priority: number) {
if (callback != null && initCallbackRegex.test(callback.toString())) {
Object.defineProperty(this, "O", {
value: originalOnChunksLoaded,
configurable: true
});
const wreq = this as WebpackInstance;
const originalCallback = callback;
callback = function (this: unknown) {
logger.info("Patched initialize app callback invoked, initializing our internal references to WebpackRequire and running beforeInitListeners");
_initWebpack(wreq);
for (const beforeInitListener of beforeInitListeners) {
beforeInitListener(wreq);
}
originalCallback.apply(this, arguments as any);
};
callback.toString = originalCallback.toString.bind(originalCallback);
arguments[2] = callback;
}
originalOnChunksLoaded.apply(this, arguments as any);
};
onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded);
// Returns whether a chunk has been loaded
Object.defineProperty(onChunksLoaded, "j", {
set(v) {
delete onChunksLoaded.j;
onChunksLoaded.j = v;
originalOnChunksLoaded.j = v;
},
configurable: true
});
}
Object.defineProperty(this, "O", {
value: onChunksLoaded,
configurable: true
});
}
});
// wreq.m is the webpack module factory. // wreq.m is the webpack module factory.
// normally, this is populated via webpackGlobal.push, which we patch below. // normally, this is populated via webpackGlobal.push, which we patch below.
// However, Discord has their .m prepopulated. // However, Discord has their .m prepopulated.
// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories // Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
//
// Update: Discord now has TWO webpack instances. Their normal one and sentry
// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules
Object.defineProperty(Function.prototype, "m", { Object.defineProperty(Function.prototype, "m", {
configurable: true, configurable: true,
set(v: any) { set(v: any) {
Object.defineProperty(this, "m", {
value: v,
configurable: true,
enumerable: true,
writable: true
});
// When using react devtools or other extensions, we may also catch their webpack here. // When using react devtools or other extensions, we may also catch their webpack here.
// This ensures we actually got the right one // This ensures we actually got the right one
const { stack } = new Error(); const { stack } = new Error();
if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(v)) { if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || Array.isArray(v)) {
logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); return;
patchFactories(v);
} }
Object.defineProperty(this, "m", { const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? "";
value: v, logger.info("Found Webpack module factory", fileName);
configurable: true
patchFactories(v);
// Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property.
// So if the setter is called, this means we can initialize the internal references to WebpackRequire.
Object.defineProperty(this, "p", {
configurable: true,
set(this: WebpackInstance, bundlePath: string) {
Object.defineProperty(this, "p", {
value: bundlePath,
configurable: true,
enumerable: true,
writable: true
});
clearTimeout(setterTimeout);
if (bundlePath !== "/assets/") return;
logger.info(`Main Webpack found in ${fileName}, initializing internal references to WebpackRequire`);
_initWebpack(this);
for (const beforeInitListener of beforeInitListeners) {
beforeInitListener(this);
}
}
}); });
// setImmediate to clear this property setter if this is not the main Webpack.
// If this is the main Webpack, wreq.p will always be set before the timeout runs.
const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0);
} }
}); });
@ -294,7 +259,6 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
for (let i = 0; i < patches.length; i++) { for (let i = 0; i < patches.length; i++) {
const patch = patches[i]; const patch = patches[i];
if (patch.predicate && !patch.predicate()) continue;
const moduleMatches = typeof patch.find === "string" const moduleMatches = typeof patch.find === "string"
? code.includes(patch.find) ? code.includes(patch.find)
@ -310,8 +274,6 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
// We change all patch.replacement to array in plugins/index // We change all patch.replacement to array in plugins/index
for (const replacement of patch.replacement as PatchReplacement[]) { for (const replacement of patch.replacement as PatchReplacement[]) {
if (replacement.predicate && !replacement.predicate()) continue;
const lastMod = mod; const lastMod = mod;
const lastCode = code; const lastCode = code;

View file

@ -38,31 +38,43 @@ export let cache: WebpackInstance["c"];
export type FilterFn = (mod: any) => boolean; export type FilterFn = (mod: any) => boolean;
type PropsFilter = Array<string>;
type CodeFilter = Array<string | RegExp>;
type StoreNameFilter = string;
const stringMatches = (s: string, filter: CodeFilter) =>
filter.every(f =>
typeof f === "string"
? s.includes(f)
: f.test(s)
);
export const filters = { export const filters = {
byProps: (...props: string[]): FilterFn => byProps: (...props: PropsFilter): FilterFn =>
props.length === 1 props.length === 1
? m => m[props[0]] !== void 0 ? m => m[props[0]] !== void 0
: m => props.every(p => m[p] !== void 0), : m => props.every(p => m[p] !== void 0),
byCode: (...code: string[]): FilterFn => m => { byCode: (...code: CodeFilter): FilterFn => {
if (typeof m !== "function") return false; code = code.map(canonicalizeMatch);
const s = Function.prototype.toString.call(m); return m => {
for (const c of code) { if (typeof m !== "function") return false;
if (!s.includes(c)) return false; return stringMatches(Function.prototype.toString.call(m), code);
} };
return true;
}, },
byStoreName: (name: string): FilterFn => m => byStoreName: (name: StoreNameFilter): FilterFn => m =>
m.constructor?.displayName === name, m.constructor?.displayName === name,
componentByCode: (...code: string[]): FilterFn => { componentByCode: (...code: CodeFilter): FilterFn => {
const filter = filters.byCode(...code); const filter = filters.byCode(...code);
return m => { return m => {
if (filter(m)) return true; if (filter(m)) return true;
if (!m.$$typeof) return false; if (!m.$$typeof) return false;
if (m.type && m.type.render) return filter(m.type.render); // memo + forwardRef if (m.type)
if (m.type) return filter(m.type); // memos return m.type.render
if (m.render) return filter(m.render); // forwardRefs ? filter(m.type.render) // memo + forwardRef
: filter(m.type); // memo
if (m.render) return filter(m.render); // forwardRef
return false; return false;
}; };
} }
@ -245,15 +257,9 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns
* Find the id of the first module factory that includes all the given code * Find the id of the first module factory that includes all the given code
* @returns string or null * @returns string or null
*/ */
export const findModuleId = traceFunction("findModuleId", function findModuleId(...code: string[]) { export const findModuleId = traceFunction("findModuleId", function findModuleId(...code: CodeFilter) {
outer:
for (const id in wreq.m) { for (const id in wreq.m) {
const str = wreq.m[id].toString(); if (stringMatches(wreq.m[id].toString(), code)) return id;
for (const c of code) {
if (!str.includes(c)) continue outer;
}
return id;
} }
const err = new Error("Didn't find module with code(s):\n" + code.join("\n")); const err = new Error("Didn't find module with code(s):\n" + code.join("\n"));
@ -272,7 +278,7 @@ export const findModuleId = traceFunction("findModuleId", function findModuleId(
* Find the first module factory that includes all the given code * Find the first module factory that includes all the given code
* @returns The module factory or null * @returns The module factory or null
*/ */
export function findModuleFactory(...code: string[]) { export function findModuleFactory(...code: CodeFilter) {
const id = findModuleId(...code); const id = findModuleId(...code);
if (!id) return null; if (!id) return null;
@ -325,7 +331,7 @@ export function findLazy(filter: FilterFn) {
/** /**
* Find the first module that has the specified properties * Find the first module that has the specified properties
*/ */
export function findByProps(...props: string[]) { export function findByProps(...props: PropsFilter) {
const res = find(filters.byProps(...props), { isIndirect: true }); const res = find(filters.byProps(...props), { isIndirect: true });
if (!res) if (!res)
handleModuleNotFound("findByProps", ...props); handleModuleNotFound("findByProps", ...props);
@ -335,7 +341,7 @@ export function findByProps(...props: string[]) {
/** /**
* Find the first module that has the specified properties, lazily * Find the first module that has the specified properties, lazily
*/ */
export function findByPropsLazy(...props: string[]) { export function findByPropsLazy(...props: PropsFilter) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByProps", props]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByProps", props]);
return proxyLazy(() => findByProps(...props)); return proxyLazy(() => findByProps(...props));
@ -344,7 +350,7 @@ export function findByPropsLazy(...props: string[]) {
/** /**
* Find the first function that includes all the given code * Find the first function that includes all the given code
*/ */
export function findByCode(...code: string[]) { export function findByCode(...code: CodeFilter) {
const res = find(filters.byCode(...code), { isIndirect: true }); const res = find(filters.byCode(...code), { isIndirect: true });
if (!res) if (!res)
handleModuleNotFound("findByCode", ...code); handleModuleNotFound("findByCode", ...code);
@ -354,7 +360,7 @@ export function findByCode(...code: string[]) {
/** /**
* Find the first function that includes all the given code, lazily * Find the first function that includes all the given code, lazily
*/ */
export function findByCodeLazy(...code: string[]) { export function findByCodeLazy(...code: CodeFilter) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByCode", code]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByCode", code]);
return proxyLazy(() => findByCode(...code)); return proxyLazy(() => findByCode(...code));
@ -363,7 +369,7 @@ export function findByCodeLazy(...code: string[]) {
/** /**
* Find a store by its displayName * Find a store by its displayName
*/ */
export function findStore(name: string) { export function findStore(name: StoreNameFilter) {
const res = find(filters.byStoreName(name), { isIndirect: true }); const res = find(filters.byStoreName(name), { isIndirect: true });
if (!res) if (!res)
handleModuleNotFound("findStore", name); handleModuleNotFound("findStore", name);
@ -373,7 +379,7 @@ export function findStore(name: string) {
/** /**
* Find a store by its displayName, lazily * Find a store by its displayName, lazily
*/ */
export function findStoreLazy(name: string) { export function findStoreLazy(name: StoreNameFilter) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["findStore", [name]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["findStore", [name]]);
return proxyLazy(() => findStore(name)); return proxyLazy(() => findStore(name));
@ -382,7 +388,7 @@ export function findStoreLazy(name: string) {
/** /**
* Finds the component which includes all the given code. Checks for plain components, memos and forwardRefs * Finds the component which includes all the given code. Checks for plain components, memos and forwardRefs
*/ */
export function findComponentByCode(...code: string[]) { export function findComponentByCode(...code: CodeFilter) {
const res = find(filters.componentByCode(...code), { isIndirect: true }); const res = find(filters.componentByCode(...code), { isIndirect: true });
if (!res) if (!res)
handleModuleNotFound("findComponentByCode", ...code); handleModuleNotFound("findComponentByCode", ...code);
@ -407,7 +413,7 @@ export function findComponentLazy<T extends object = any>(filter: FilterFn) {
/** /**
* Finds the first component that includes all the given code, lazily * Finds the first component that includes all the given code, lazily
*/ */
export function findComponentByCodeLazy<T extends object = any>(...code: string[]) { export function findComponentByCodeLazy<T extends object = any>(...code: CodeFilter) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponentByCode", code]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponentByCode", code]);
return LazyComponent<T>(() => { return LazyComponent<T>(() => {
@ -421,7 +427,7 @@ export function findComponentByCodeLazy<T extends object = any>(...code: string[
/** /**
* Finds the first component that is exported by the first prop name, lazily * Finds the first component that is exported by the first prop name, lazily
*/ */
export function findExportedComponentLazy<T extends object = any>(...props: string[]) { export function findExportedComponentLazy<T extends object = any>(...props: PropsFilter) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["findExportedComponent", props]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["findExportedComponent", props]);
return LazyComponent<T>(() => { return LazyComponent<T>(() => {
@ -445,10 +451,13 @@ export function findExportedComponentLazy<T extends object = any>(...props: stri
* closeModal: filters.byCode("key==") * closeModal: filters.byCode("key==")
* }) * })
*/ */
export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule<S extends string>(code: string, mappers: Record<S, FilterFn>): Record<S, any> { export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>): Record<S, any> {
if (!Array.isArray(code)) code = [code];
code = code.map(canonicalizeMatch);
const exports = {} as Record<S, any>; const exports = {} as Record<S, any>;
const id = findModuleId(code); const id = findModuleId(...code);
if (id === null) if (id === null)
return exports; return exports;
@ -482,7 +491,7 @@ export const mapMangledModule = traceFunction("mapMangledModule", function mapMa
* closeModal: filters.byCode("key==") * closeModal: filters.byCode("key==")
* }) * })
*/ */
export function mapMangledModuleLazy<S extends string>(code: string, mappers: Record<S, FilterFn>): Record<S, any> { export function mapMangledModuleLazy<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>): Record<S, any> {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["mapMangledModule", [code, mappers]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["mapMangledModule", [code, mappers]]);
return proxyLazy(() => mapMangledModule(code, mappers)); return proxyLazy(() => mapMangledModule(code, mappers));
@ -497,7 +506,7 @@ export const ChunkIdsRegex = /\("([^"]+?)"\)/g;
* @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory
* @returns A promise that resolves with a boolean whether the chunks were loaded * @returns A promise that resolves with a boolean whether the chunks were loaded
*/ */
export async function extractAndLoadChunks(code: string[], matcher: RegExp = DefaultExtractAndLoadChunksRegex) { export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = DefaultExtractAndLoadChunksRegex) {
const module = findModuleFactory(...code); const module = findModuleFactory(...code);
if (!module) { if (!module) {
const err = new Error("extractAndLoadChunks: Couldn't find module factory"); const err = new Error("extractAndLoadChunks: Couldn't find module factory");
@ -562,7 +571,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
* @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory
* @returns A function that returns a promise that resolves with a boolean whether the chunks were loaded, on first call * @returns A function that returns a promise that resolves with a boolean whether the chunks were loaded, on first call
*/ */
export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtractAndLoadChunksRegex) { export function extractAndLoadChunksLazy(code: CodeFilter, matcher = DefaultExtractAndLoadChunksRegex) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]);
return makeLazy(() => extractAndLoadChunks(code, matcher)); return makeLazy(() => extractAndLoadChunks(code, matcher));
@ -572,7 +581,7 @@ export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtrac
* Wait for a module that matches the provided filter to be registered, * Wait for a module that matches the provided filter to be registered,
* then call the callback with the module as the first argument * then call the callback with the module as the first argument
*/ */
export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) { export function waitFor(filter: string | PropsFilter | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) {
if (IS_REPORTER && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]); if (IS_REPORTER && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]);
if (typeof filter === "string") if (typeof filter === "string")
@ -593,21 +602,18 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback
/** /**
* Search modules by keyword. This searches the factory methods, * Search modules by keyword. This searches the factory methods,
* meaning you can search all sorts of things, displayName, methodName, strings somewhere in the code, etc * meaning you can search all sorts of things, displayName, methodName, strings somewhere in the code, etc
* @param filters One or more strings or regexes * @param code One or more strings or regexes
* @returns Mapping of found modules * @returns Mapping of found modules
*/ */
export function search(...filters: Array<string | RegExp>) { export function search(...code: CodeFilter) {
const results = {} as Record<number, Function>; const results = {} as Record<number, Function>;
const factories = wreq.m; const factories = wreq.m;
outer:
for (const id in factories) { for (const id in factories) {
const factory = factories[id].original ?? factories[id]; const factory = factories[id].original ?? factories[id];
const str: string = factory.toString();
for (const filter of filters) { if (stringMatches(factory.toString(), code))
if (typeof filter === "string" && !str.includes(filter)) continue outer; results[id] = factory;
if (filter instanceof RegExp && !filter.test(str)) continue outer;
}
results[id] = factory;
} }
return results; return results;