Merge branch 'immediate-finds' into immediate-finds-modules-proxy

This commit is contained in:
Nuckyz 2024-06-27 06:40:56 -03:00
commit 057ccfde11
No known key found for this signature in database
GPG key ID: 440BF8296E1C4AD9
41 changed files with 507 additions and 454 deletions

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

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

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

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

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

View file

@ -290,3 +290,42 @@ 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>
);
}

View file

@ -40,7 +40,7 @@ import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextI
import Plugins, { ExcludedPlugins } from "~plugins"; import Plugins, { ExcludedPlugins } from "~plugins";
// Avoid circular dependency // Avoid circular dependency
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins")) as typeof import("../../plugins"); const PluginManager = proxyLazy(() => require("../../plugins")) as typeof import("../../plugins");
const cl = classNameFactory("vc-plugins-"); const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189"); const logger = new Logger("PluginSettings", "#a6d189");
@ -109,7 +109,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
// If we're enabling a plugin, make sure all deps are enabled recursively. // If we're enabling a plugin, make sure all deps are enabled recursively.
if (!wasEnabled) { if (!wasEnabled) {
const { restartNeeded, failures } = startDependenciesRecursive(plugin); const { restartNeeded, failures } = PluginManager.startDependenciesRecursive(plugin);
if (failures.length) { if (failures.length) {
logger.error(`Failed to start dependencies for ${plugin.name}: ${failures.join(", ")}`); logger.error(`Failed to start dependencies for ${plugin.name}: ${failures.join(", ")}`);
showNotice("Failed to start dependencies: " + failures.join(", "), "Close", () => null); showNotice("Failed to start dependencies: " + failures.join(", "), "Close", () => null);
@ -135,7 +135,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
return; return;
} }
const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin); const result = wasEnabled ? PluginManager.stopPlugin(plugin) : PluginManager.startPlugin(plugin);
if (!result) { if (!result) {
settings.enabled = false; settings.enabled = false;

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

@ -7,10 +7,10 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByProps } from "@webpack"; import { findByPropsAndExtract } from "@webpack";
import { Button, ChannelStore, Text } from "@webpack/common"; import { Button, ChannelStore, Text } from "@webpack/common";
const { selectChannel } = findByProps("selectChannel", "selectVoiceChannel"); const selectChannel = findByPropsAndExtract("selectChannel", "selectVoiceChannel");
function jumpToMessage(channelId: string, messageId: string) { function jumpToMessage(channelId: string, messageId: string) {
const guildId = ChannelStore.getChannel(channelId)?.guild_id; const guildId = ChannelStore.getChannel(channelId)?.guild_id;

View file

@ -8,10 +8,10 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { copyWithToast } from "@utils/misc"; import { copyWithToast } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByProps } from "@webpack"; import { findByPropsAndExtract } from "@webpack";
import { Menu } from "@webpack/common"; import { Menu } from "@webpack/common";
const { convertNameToSurrogate } = findByProps("convertNameToSurrogate"); const convertNameToSurrogate = findByPropsAndExtract("convertNameToSurrogate");
interface Emoji { interface Emoji {
type: string; type: string;

View file

@ -5,7 +5,7 @@
*/ */
import { NoopComponent } from "@utils/react"; import { NoopComponent } from "@utils/react";
import { filters, findComponent } from "@webpack"; import { findComponentByCode } from "@webpack";
import { React } from "@webpack/common"; import { React } from "@webpack/common";
import type { ComponentType, HTMLProps, PropsWithChildren } from "react"; import type { ComponentType, HTMLProps, PropsWithChildren } from "react";
@ -19,7 +19,7 @@ type DecorationGridItemComponent = ComponentType<PropsWithChildren<HTMLProps<HTM
export let DecorationGridItem: DecorationGridItemComponent = NoopComponent; export let DecorationGridItem: DecorationGridItemComponent = NoopComponent;
export const setDecorationGridItem = v => DecorationGridItem = v; export const setDecorationGridItem = v => DecorationGridItem = v;
export const AvatarDecorationModalPreview = findComponent(filters.byComponentCode(".shopPreviewBanner"), component => { export const AvatarDecorationModalPreview = findComponentByCode(".shopPreviewBanner", component => {
return React.memo(component); return React.memo(component);
}); });

View file

@ -9,7 +9,7 @@ import { Link } from "@components/Link";
import { openInviteModal } from "@utils/discord"; import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { filters, findComponentByCode, mapMangledModule } from "@webpack"; import { findByProps, findComponentByCode } from "@webpack";
import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common"; import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common";
import { GUILD_ID, INVITE_KEY, RAW_SKU_ID } from "../../lib/constants"; import { GUILD_ID, INVITE_KEY, RAW_SKU_ID } from "../../lib/constants";
@ -18,11 +18,8 @@ import { cl, DecorationModalStyles, requireAvatarDecorationModal, requireCreateS
import { AvatarDecorationModalPreview } from "../components"; import { AvatarDecorationModalPreview } from "../components";
const FileUpload = findComponentByCode("fileUploadInput,"); const FileUpload = findComponentByCode("fileUploadInput,");
const HelpMessage = findComponentByCode(".iconDiv,", "messageType");
const { HelpMessage, HelpMessageTypes } = mapMangledModule('POSITIVE=3]="POSITIVE', { const HelpMessageTypes = findByProps("POSITIVE", "WARNING");
HelpMessageTypes: filters.byProps("POSITIVE", "WARNING"),
HelpMessage: filters.byCode(".iconDiv")
});
function useObjectURL(object: Blob | MediaSource | null) { function useObjectURL(object: Blob | MediaSource | null) {
const [url, setUrl] = useState<string | null>(null); const [url, setUrl] = useState<string | null>(null);

View file

@ -109,9 +109,9 @@ interface ProfileModalProps {
} }
const ColorPicker = findComponentByCode<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); const ColorPicker = findComponentByCode<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
const ProfileModal = findComponentByCode<ProfileModalProps>('"ProfileCustomizationPreview"'); const ProfileModal = findComponentByCode<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

@ -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,13 +16,20 @@
* 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 { findByCode, findByProps, mapMangledModule } from "@webpack"; import { findByCode, findByPropsAndExtract, mapMangledModule } from "@webpack";
import { Menu } from "@webpack/common";
import { Guild } from "discord-types/general";
const { updateGuildNotificationSettings } = findByProps("updateGuildNotificationSettings"); const updateGuildNotificationSettings = findByPropsAndExtract("updateGuildNotificationSettings");
const { toggleShowAllChannels } = mapMangledModule(".onboardExistingMember(", { const OnboardingChannelUtils = mapMangledModule(".onboardExistingMember(", {
toggleShowAllChannels: m => { toggleShowAllChannels: m => {
const s = String(m); const s = String(m);
return s.length < 100 && !s.includes("onboardExistingMember") && !s.includes("getOptedInChannels"); return s.length < 100 && !s.includes("onboardExistingMember") && !s.includes("getOptedInChannels");
@ -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)) {
OnboardingChannelUtils.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

@ -42,7 +42,7 @@ const RoleClasses3 = findByProps("roleNameOverflow", "root", "roleName", "roleRe
const Classes = proxyLazy(() => Object.assign({}, RoleClasses1, RoleClasses2, RoleClasses3)); const Classes = proxyLazy(() => Object.assign({}, RoleClasses1, RoleClasses2, RoleClasses3));
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(() => {
@ -94,6 +94,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 { findByProps } 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 = findByProps("container", "scroller", "list");
const RoleButtonClasses = findByProps("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 { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
@ -27,7 +29,20 @@ import { findByProps, findStore } 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 = findStore("SessionsStore"); export interface Session {
sessionId: string;
status: string;
active: boolean;
clientInfo: {
version: number;
os: string;
client: string;
};
}
const SessionsStore = findStore("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

@ -14,7 +14,7 @@ import { Timestamp } from "@webpack/common";
import type { Message } from "discord-types/general"; import type { Message } from "discord-types/general";
import type { HTMLAttributes } from "react"; import type { HTMLAttributes } from "react";
const { calendarFormat, dateFormat, isSameDay } = mapMangledModule("millisecondsInUnit:", { const DateFormatUtils = mapMangledModule("millisecondsInUnit:", {
calendarFormat: filters.byCode("sameElse"), calendarFormat: filters.byCode("sameElse"),
dateFormat: filters.byCode(':").concat'), dateFormat: filters.byCode(':").concat'),
isSameDay: filters.byCode("Math.abs(+"), isSameDay: filters.byCode("Math.abs(+"),
@ -46,14 +46,14 @@ function ReplyTimestamp({
return ( return (
<Timestamp <Timestamp
className="vc-reply-timestamp" className="vc-reply-timestamp"
compact={isSameDay(refTimestamp, baseTimestamp)} compact={DateFormatUtils.isSameDay(refTimestamp, baseTimestamp)}
timestamp={refTimestamp} timestamp={refTimestamp}
isInline={false} isInline={false}
> >
<Sep>[</Sep> <Sep>[</Sep>
{isSameDay(refTimestamp, baseTimestamp) {DateFormatUtils.isSameDay(refTimestamp, baseTimestamp)
? dateFormat(refTimestamp, "LT") ? DateFormatUtils.dateFormat(refTimestamp, "LT")
: calendarFormat(refTimestamp) : DateFormatUtils.calendarFormat(refTimestamp)
} }
<Sep>]</Sep> <Sep>]</Sep>
</Timestamp> </Timestamp>

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 { findByProps } 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 = findByProps("container", "scroller", "list");
const RoleButtonClasses = findByProps("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: ".VIEW_FULL_PROFILE,",
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

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

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

@ -21,7 +21,7 @@ import { findByProps, webpackDependantLazy } from "@webpack";
import { Flux, FluxDispatcher } from "@webpack/common"; import { Flux, FluxDispatcher } from "@webpack/common";
// Avoid circular dependency // Avoid circular dependency
const { settings } = proxyLazy(() => require(".")) as typeof import("."); const SpotifyControls = proxyLazy(() => require(".")) as typeof import(".");
export interface Track { export interface Track {
id: string; id: string;
@ -91,7 +91,7 @@ export const SpotifyStore = webpackDependantLazy(() => {
public isSettingPosition = false; public isSettingPosition = false;
public openExternal(path: string) { public openExternal(path: string) {
const url = settings.store.useSpotifyUris || Vencord.Plugins.isPluginEnabled("OpenInApp") const url = SpotifyControls.settings.store.useSpotifyUris || Vencord.Plugins.isPluginEnabled("OpenInApp")
? "spotify:" + path.replaceAll("/", (_, idx) => idx === 0 ? "" : ":") ? "spotify:" + path.replaceAll("/", (_, idx) => idx === 0 ? "" : ":")
: "https://open.spotify.com" + path; : "https://open.spotify.com" + path;

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

@ -22,11 +22,12 @@ import { isNonNullish } from "@utils/guards";
import { sleep } from "@utils/misc"; import { sleep } from "@utils/misc";
import { Queue } from "@utils/Queue"; import { Queue } from "@utils/Queue";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { webpackDependantLazy } from "@webpack";
import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common"; import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common";
import { type ComponentType, type ReactNode } from "react"; import { type ComponentType, type ReactNode } from "react";
// LYING to the type checker here // LYING to the type checker here
const UserFlags = Constants.UserFlags as Record<string, number>; const UserFlags = webpackDependantLazy(() => Constants.UserFlags as Record<string, number>);
const badges: Record<string, ProfileBadge> = { const badges: Record<string, ProfileBadge> = {
active_developer: { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" }, active_developer: { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" },
bug_hunter_level_1: { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" }, bug_hunter_level_1: { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },

View file

@ -70,16 +70,16 @@ const handler: ProxyHandler<any> = {
* Wraps the result of factory in a Proxy you can consume as if it wasn't lazy. * Wraps the result of factory in a Proxy you can consume as if it wasn't lazy.
* On first property access, the factory is evaluated. * On first property access, the factory is evaluated.
* *
* IMPORTANT:
* Destructuring at top level is not supported for proxyLazy.
*
* @param factory Factory returning the result * @param factory Factory returning the result
* @param attempts How many times to try to evaluate the factory before giving up * @param attempts How many times to try to evaluate the factory before giving up
* @returns Result of factory function * @returns Result of factory function
*/ */
export function proxyLazy<T = AnyObject>(factory: () => T, attempts = 5, isChild = false): ProxyLazy<T> { export function proxyLazy<T = AnyObject>(factory: () => T, attempts = 5): ProxyLazy<T> {
const get = makeLazy(factory, attempts, { isIndirect: true }); const get = makeLazy(factory, attempts, { isIndirect: true });
let isSameTick = true;
if (!isChild) setTimeout(() => isSameTick = false, 0);
const proxyDummy = Object.assign(function () { }, { const proxyDummy = Object.assign(function () { }, {
[SYM_LAZY_GET]() { [SYM_LAZY_GET]() {
if (!proxyDummy[SYM_LAZY_CACHED]) { if (!proxyDummy[SYM_LAZY_CACHED]) {
@ -108,27 +108,12 @@ export function proxyLazy<T = AnyObject>(factory: () => T, attempts = 5, isChild
return Reflect.get(target, p, receiver); return Reflect.get(target, p, receiver);
} }
// If we're still in the same tick, it means the lazy was immediately used.
// thus, we lazy proxy the get access to make things like destructuring work as expected
// meow here will also be a lazy
// `const { meow } = proxyLazy(() => ({ meow: [] }));`
if (!isChild && isSameTick) {
return proxyLazy(
() => {
const lazyTarget = target[SYM_LAZY_GET]();
return Reflect.get(lazyTarget, p, lazyTarget);
},
attempts,
true
);
}
const lazyTarget = target[SYM_LAZY_GET](); const lazyTarget = target[SYM_LAZY_GET]();
if (typeof lazyTarget === "object" || typeof lazyTarget === "function") { if (typeof lazyTarget === "object" || typeof lazyTarget === "function") {
return Reflect.get(lazyTarget, p, lazyTarget); return Reflect.get(lazyTarget, p, lazyTarget);
} }
throw new Error("proxyLazy called on a primitive value. This can happen if you try to destructure a primitive at the same tick as the proxy was created."); throw new Error("proxyLazy called on a primitive value.");
} }
}); });

View file

@ -45,18 +45,17 @@ const handler: ProxyHandler<any> = {
* A proxy which has an inner value that can be set later. * A proxy which has an inner value that can be set later.
* When a property is accessed, the proxy looks for the property value in its inner value, and errors if it's not set. * When a property is accessed, the proxy looks for the property value in its inner value, and errors if it's not set.
* *
* IMPORTANT:
* Destructuring at top level is not supported for proxyInner.
*
* @param err The error message to throw when the inner value is not set * @param err The error message to throw when the inner value is not set
* @param primitiveErr The error message to throw when the inner value is a primitive * @param primitiveErr The error message to throw when the inner value is a primitive
* @returns A proxy which will act like the inner value when accessed * @returns A proxy which will act like the inner value when accessed
*/ */
export function proxyInner<T = AnyObject>( export function proxyInner<T = AnyObject>(
errMsg = "Proxy inner value is undefined, setInnerValue was never called.", errMsg = "Proxy inner value is undefined, setInnerValue was never called.",
primitiveErrMsg = "proxyInner called on a primitive value. This can happen if you try to destructure a primitive at the same tick as the proxy was created.", primitiveErrMsg = "proxyInner called on a primitive value."
isChild = false
): [proxy: ProxyInner<T>, setInnerValue: (innerValue: T) => void] { ): [proxy: ProxyInner<T>, setInnerValue: (innerValue: T) => void] {
let isSameTick = true;
if (!isChild) setTimeout(() => isSameTick = false, 0);
const proxyDummy = Object.assign(function () { }, { const proxyDummy = Object.assign(function () { }, {
[SYM_PROXY_INNER_GET]: function () { [SYM_PROXY_INNER_GET]: function () {
if (proxyDummy[SYM_PROXY_INNER_VALUE] == null) { if (proxyDummy[SYM_PROXY_INNER_VALUE] == null) {
@ -75,24 +74,6 @@ export function proxyInner<T = AnyObject>(
return Reflect.get(target, p, receiver); return Reflect.get(target, p, receiver);
} }
// If we're still in the same tick, it means the proxy was immediately used.
// And, if the inner value is still nullish, it means the proxy was used before setInnerValue was called.
// So, proxy the get access to make things like destructuring work as expected.
// We dont need to proxy if the inner value is available, and recursiveSetInnerValue won't ever be called anyways,
// because the top setInnerValue was called before we proxied the get access
// example here will also be a proxy:
// `const { example } = findByProps("example");`
if (isSameTick && !isChild && proxyDummy[SYM_PROXY_INNER_VALUE] == null) {
const [recursiveProxy, recursiveSetInnerValue] = proxyInner(errMsg, primitiveErrMsg, true);
recursiveSetInnerValues.push((innerValue: T) => {
// Set the inner value of the destructured value as the prop value p of the parent
recursiveSetInnerValue(Reflect.get(innerValue as object, p, innerValue));
});
return recursiveProxy;
}
const innerTarget = target[SYM_PROXY_INNER_GET](); const innerTarget = target[SYM_PROXY_INNER_GET]();
if (typeof innerTarget === "object" || typeof innerTarget === "function") { if (typeof innerTarget === "object" || typeof innerTarget === "function") {
return Reflect.get(innerTarget, p, innerTarget); return Reflect.get(innerTarget, p, innerTarget);
@ -102,14 +83,8 @@ export function proxyInner<T = AnyObject>(
} }
}); });
// Values destructured in the same tick the proxy was created will push their setInnerValue here
const recursiveSetInnerValues = [] as Array<(innerValue: T) => void>;
// Once we set the parent inner value, we will call the setInnerValue functions of the destructured values,
// for them to get the proper value from the parent and use as their inner instead
function setInnerValue(innerValue: T) { function setInnerValue(innerValue: T) {
proxyDummy[SYM_PROXY_INNER_VALUE] = innerValue; proxyDummy[SYM_PROXY_INNER_VALUE] = innerValue;
recursiveSetInnerValues.forEach(setInnerValue => setInnerValue(innerValue));
// Avoid binding toString if the inner value is null. // Avoid binding toString if the inner value is null.
// This can happen if we are setting the inner value as another instance of proxyInner, which will cause that proxy to instantly evaluate and throw an error // This can happen if we are setting the inner value as another instance of proxyInner, which will cause that proxy to instantly evaluate and throw an error

View file

@ -135,7 +135,7 @@ export const filters = {
} }
}; };
export const webpackSearchHistory = [] as Array<["waitFor" | "find" | "findComponent" | "findExportedComponent" | "findComponentByCode" | "findByProps" | "findByCode" | "findStore" | "findByFactoryCode" | "mapMangledModule" | "extractAndLoadChunks" | "webpackDependantLazy" | "webpackDependantLazyComponent", any[]]>; export const webpackSearchHistory = [] as Array<["waitFor" | "find" | "findComponent" | "findExportedComponent" | "findComponentByCode" | "findByProps" | "findByPropsAndExtract" | "findByCode" | "findStore" | "findByFactoryCode" | "mapMangledModule" | "extractAndLoadChunks" | "webpackDependantLazy" | "webpackDependantLazyComponent", any[]]>;
function printFilter(filter: FilterFn) { function printFilter(filter: FilterFn) {
if (filter.$$vencordProps != null) { if (filter.$$vencordProps != null) {
@ -188,26 +188,26 @@ export function waitFor(filter: FilterFn, callback: ModCallbackFn, { isIndirect
* *
* The way this works internally is: * The way this works internally is:
* Wait for the first export or module exports that matches the provided filter to be required, * Wait for the first export or module exports that matches the provided filter to be required,
* then call the callback with the export or module exports as the first argument. * then call the parse function with the export or module exports as the first argument.
* *
* If the module containing the export(s) is already required, the callback will be called immediately. * If the module containing the export(s) is already required, the parse function will be called immediately.
* *
* The callback must return a value that will be used as the proxy inner value. * The parse function must return a value that will be used as the proxy inner value.
* *
* If no callback is specified, the default callback will assign the proxy inner value to the plain find result * If no parse function is specified, the default parse will assign the proxy inner value to the plain find result.
* *
* @param filter A function that takes an export or module exports and returns a boolean * @param filter A function that takes an export or module exports and returns a boolean
* @param callback A function that takes the find result as its first argument and returns something to use as the proxy inner value. Useful if you want to use a value from the find result, instead of all of it. Defaults to the find result itself * @param parse A function that takes the find result as its first argument and returns something to use as the proxy inner value. Useful if you want to use a value from the find result, instead of all of it. Defaults to the find result itself
* @returns A proxy that has the callback return value as its true value, or the callback return value if the callback was called when the function was called * @returns A proxy that has the parse function return value as its true value, or the plain parse function return value if it was called immediately.
*/ */
export function find<T = AnyObject>(filter: FilterFn, callback: (module: ModuleExports) => any = m => m, { isIndirect = false }: { isIndirect?: boolean; } = {}) { export function find<T = AnyObject>(filter: FilterFn, parse: (module: ModuleExports) => ModuleExports = m => m, { isIndirect = false }: { isIndirect?: boolean; } = {}) {
if (typeof filter !== "function") if (typeof filter !== "function")
throw new Error("Invalid filter. Expected a function got " + typeof filter); throw new Error("Invalid filter. Expected a function got " + typeof filter);
if (typeof callback !== "function") if (typeof parse !== "function")
throw new Error("Invalid callback. Expected a function got " + typeof callback); throw new Error("Invalid find parse. Expected a function got " + typeof parse);
const [proxy, setInnerValue] = proxyInner<T>(`Webpack find matched no module. Filter: ${printFilter(filter)}`, "Webpack find with proxy called on a primitive value. This can happen if you try to destructure a primitive in the top level definition of the find."); const [proxy, setInnerValue] = proxyInner<T>(`Webpack find matched no module. Filter: ${printFilter(filter)}`, "Webpack find with proxy called on a primitive value.");
waitFor(filter, m => setInnerValue(callback(m)), { isIndirect: true }); waitFor(filter, m => setInnerValue(parse(m)), { isIndirect: true });
if (IS_REPORTER && !isIndirect) { if (IS_REPORTER && !isIndirect) {
webpackSearchHistory.push(["find", [proxy, filter]]); webpackSearchHistory.push(["find", [proxy, filter]]);
@ -332,12 +332,37 @@ export function findComponentByCode<T extends object = any>(...code: string[] |
* Find the first module exports or export that includes all the given props. * Find the first module exports or export that includes all the given props.
* *
* @param props A list of props to search the module or exports for * @param props A list of props to search the module or exports for
* @param parse A function that takes the find result as its first argument and returns something. Useful if you want to use a value from the find result, instead of all of it. Defaults to the find result itself
*/ */
export function findByProps<T = AnyObject>(...props: string[]) { export function findByProps<T = AnyObject>(...props: string[] | [...string[], (module: ModuleExports) => T]) {
const result = find<T>(filters.byProps(...props), m => m, { isIndirect: true }); const parse = (typeof props.at(-1) === "function" ? props.pop() : m => m) as (module: ModuleExports) => T;
const newProps = props as string[];
const result = find<T>(filters.byProps(...newProps), parse, { isIndirect: true });
if (IS_REPORTER) { if (IS_REPORTER) {
webpackSearchHistory.push(["findByProps", [result, ...props]]); webpackSearchHistory.push(["findByProps", [result, ...newProps]]);
}
return result;
}
/**
* Find the first prop value defined by the first prop name, which is in a module exports or export including all the given props.
*
* @example const getUser = findByPropsAndExtract("getUser", "fetchUser")
*
* @param props A list of props to search the module or exports for
* @param parse A function that takes the find result as its first argument and returns something. Useful if you want to use a value from the find result, instead of all of it. Defaults to the find result itself
*/
export function findByPropsAndExtract<T = AnyObject>(...props: string[] | [...string[], (module: ModuleExports) => T]) {
const parse = (typeof props.at(-1) === "function" ? props.pop() : m => m) as (module: ModuleExports) => T;
const newProps = props as string[];
const result = find<T>(filters.byProps(...newProps), m => parse(m[newProps[0]]), { isIndirect: true });
if (IS_REPORTER) {
webpackSearchHistory.push(["findByPropsAndExtract", [result, ...newProps]]);
} }
return result; return result;
@ -347,12 +372,16 @@ export function findByProps<T = AnyObject>(...props: string[]) {
* Find the first export that includes all the given code. * Find the first export that includes all the given code.
* *
* @param code A list of code to search each export for * @param code A list of code to search each export for
* @param parse A function that takes the find result as its first argument and returns something. Useful if you want to use a value from the find result, instead of all of it. Defaults to the find result itself
*/ */
export function findByCode<T = AnyObject>(...code: string[]) { export function findByCode<T = AnyObject>(...code: string[] | [...string[], (module: ModuleExports) => T]) {
const result = find<T>(filters.byCode(...code), m => m, { isIndirect: true }); const parse = (typeof code.at(-1) === "function" ? code.pop() : m => m) as (module: ModuleExports) => T;
const newCode = code as string[];
const result = find<T>(filters.byCode(...newCode), parse, { isIndirect: true });
if (IS_REPORTER) { if (IS_REPORTER) {
webpackSearchHistory.push(["findByCode", [result, ...code]]); webpackSearchHistory.push(["findByCode", [result, ...newCode]]);
} }
return result; return result;
@ -377,12 +406,16 @@ export function findStore<T = GenericStore>(name: string) {
* Find the module exports of the first module which the factory includes all the given code. * Find the module exports of the first module which the factory includes all the given code.
* *
* @param code A list of code to search each factory for * @param code A list of code to search each factory for
* @param parse A function that takes the find result as its first argument and returns something. Useful if you want to use a value from the find result, instead of all of it. Defaults to the find result itself
*/ */
export function findByFactoryCode<T = AnyObject>(...code: string[]) { export function findByFactoryCode<T = AnyObject>(...code: string[] | [...string[], (module: ModuleExports) => T]) {
const result = find<T>(filters.byFactoryCode(...code), m => m, { isIndirect: true }); const parse = (typeof code.at(-1) === "function" ? code.pop() : m => m) as (module: ModuleExports) => T;
const newCode = code as string[];
const result = find<T>(filters.byFactoryCode(...newCode), parse, { isIndirect: true });
if (IS_REPORTER) { if (IS_REPORTER) {
webpackSearchHistory.push(["findByFactoryCode", [result, ...code]]); webpackSearchHistory.push(["findByFactoryCode", [result, ...newCode]]);
} }
return result; return result;
@ -421,7 +454,7 @@ export function mapMangledModule<S extends PropertyKey>(code: string | string[],
} }
} }
const [proxy] = proxyInner(`Webpack mapMangledModule mapper filter matched no module. Filter: ${printFilter(filter)}`, "Webpack find with proxy called on a primitive value. This can happen if you try to destructure a primitive in the top level definition of the find."); const [proxy] = proxyInner(`Webpack mapMangledModule mapper filter matched no module. Filter: ${printFilter(filter)}`, "Webpack find with proxy called on a primitive value.");
// Use the proxy to throw errors because no export matched the filter // Use the proxy to throw errors because no export matched the filter
mapping[newName] = proxy; mapping[newName] = proxy;
} }
@ -442,7 +475,7 @@ export function mapMangledModule<S extends PropertyKey>(code: string | string[],
export function findModuleFactory(...code: string[]) { export function findModuleFactory(...code: string[]) {
const filter = filters.byFactoryCode(...code); const filter = filters.byFactoryCode(...code);
const [proxy, setInnerValue] = proxyInner<AnyModuleFactory>(`Webpack module factory find matched no module. Filter: ${printFilter(filter)}`, "Webpack find with proxy called on a primitive value. This can happen if you try to destructure a primitive in the top level definition of the find."); const [proxy, setInnerValue] = proxyInner<AnyModuleFactory>(`Webpack module factory find matched no module. Filter: ${printFilter(filter)}`, "Webpack find with proxy called on a primitive value.");
waitFor(filter, (_, { factory }) => setInnerValue(factory)); waitFor(filter, (_, { factory }) => setInnerValue(factory));
if (proxy[SYM_PROXY_INNER_VALUE] != null) return proxy[SYM_PROXY_INNER_VALUE] as ProxyInner<AnyModuleFactory>; if (proxy[SYM_PROXY_INNER_VALUE] != null) return proxy[SYM_PROXY_INNER_VALUE] as ProxyInner<AnyModuleFactory>;

View file

@ -26,6 +26,7 @@ export let Card: t.Card = NoopComponent as any;
export let Button: t.Button = NoopComponent as any; export let Button: t.Button = NoopComponent as any;
export let Switch: t.Switch = NoopComponent; export let Switch: t.Switch = NoopComponent;
export let Tooltip: t.Tooltip = NoopComponent as any; export let Tooltip: t.Tooltip = NoopComponent as any;
export let TooltipContainer: t.TooltipContainer = NoopComponent as any;
export let TextInput: t.TextInput = NoopComponent as any; export let TextInput: t.TextInput = NoopComponent as any;
export let TextArea: t.TextArea = NoopComponent; export let TextArea: t.TextArea = NoopComponent;
export let Text: t.Text = NoopComponent; export let Text: t.Text = NoopComponent;
@ -57,6 +58,7 @@ export const Forms = find<t.Forms>(filters.byProps("FormItem", "Button"), m => {
Button, Button,
FormSwitch: Switch, FormSwitch: Switch,
Tooltip, Tooltip,
TooltipContainer,
TextInput, TextInput,
TextArea, TextArea,
Text, Text,

View file

@ -108,6 +108,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;
@ -117,6 +139,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;
@ -382,7 +424,7 @@ export type Popout = ComponentType<{
Animation: typeof PopoutAnimation; Animation: typeof PopoutAnimation;
}; };
export type Dialog = ComponentType<PropsWithChildren<any>>; export type Dialog = ComponentType<JSX.IntrinsicElements["div"]>;
export type TabBar = ComponentType<PropsWithChildren<any>> & { export type TabBar = ComponentType<PropsWithChildren<any>> & {
Header: ComponentType<PropsWithChildren<any>>; Header: ComponentType<PropsWithChildren<any>>;