Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
thororen1234 2026-08-09 23:29:18 -04:00
commit 94c0ac8e16
No known key found for this signature in database
13 changed files with 70 additions and 422 deletions

View file

@ -1,22 +0,0 @@
code_review:
disable: false
comment_severity_threshold: MEDIUM
max_review_comments: -1
pull_request_opened:
help: false
summary: false
code_review: false
include_drafts: false
ignore_patterns:
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "dist/**"
- "browser/**"
- "packages/**"
- "patches/**"
- "misc/**"
- "scripts/**"
- ".vscode/**"
- ".github/**"
- "*.map"

View file

@ -1,365 +0,0 @@
# Equicord Code Review Style Guide
You are reviewing PRs for **Equicord**, a Discord client mod built on Vencord. It uses TypeScript, React, and webpack patching to inject into Discord. The codebase has extensive built-in utilities that contributors MUST use. Be direct, actionable, no pleasantries. Use natural, human language. Be blunt when code is bad.
---
## Plugin Acceptance (Instant Reject)
If a plugin breaks ANY of these, reject the entire PR. These are non-negotiable:
1. **No simple slash-command plugins** (e.g. `/cat`). Should be a [user-installable Discord app](https://discord.com/developers/docs/change-log#userinstallable-apps-preview) instead.
2. **No simple text replacement plugins.** The built-in TextReplace plugin already covers this.
3. **No raw DOM manipulation / MutationObserver.** Always use patches and React.
4. **No FakeDeafen or FakeMute.**
5. **No StereoMic-related plugins.**
6. **No UI-only hide/redesign plugins.** Use CSS for that. (Negotiable in rare cases.)
7. **No plugins targeting specific third-party Discord bots.** Official Discord apps are fine.
8. **No selfbot or API abuse.** Auto-replies, animated statuses, message pruning, Nitro snipers, etc.
9. **No untrusted third-party APIs.** Well-known services (Google, GitHub) are acceptable.
10. **No plugins requiring users to provide their own API keys.**
11. **No new dependencies** unless strictly necessary and well justified.
---
## Plugin Structure
Default export via `definePlugin` from `@utils/types`. Non-negotiable.
```typescript
import definePlugin from "@utils/types";
import { EquicordDevs } from "@utils/constants";
export default definePlugin({
name: "PluginName", // PascalCase, matches directory name
description: "Does something", // Capital first
authors: [EquicordDevs.Name], // EquicordDevs for new, Devs for upstream
});
```
**Prefer declarative APIs** over manual registration:
- `flux: { EVENT_NAME(data) {} }` not manual FluxDispatcher subscribe
- `contextMenus: { "nav-id": fn }` not manual addContextMenuPatch
- `chatBarButton: { render, icon }` not addChatBarButton
- `messagePopoverButton: { render, icon }` not addMessagePopoverButton
- `managedStyle` for CSS not manual enableStyle/disableStyle in start/stop
**Reject deprecated fields:** `renderChatBarButton`, `renderMessagePopoverButton`, `options`
**Lifecycle balance:** anything registered in `start()` must be cleaned up in `stop()`.
---
## Forbidden Patterns
Flag these and suggest the fix:
| Bad | Good | Reason |
| ---------------------------------------------------------------- | ------------------------------------------- | ----------------------------------- |
| `value !== null && value !== undefined` | `value` or `value != null` | Verbose |
| `array && array.length > 0` | `array?.length` | Redundant check |
| `settings?.store?.value` | `settings.store.value` | Store is always defined |
| `value \|\| defaultValue` | `value ?? defaultValue` | `\|\|` falsifies `0`, `""`, `false` |
| `` `${classA} ${classB}` `` | `classes(classA, classB)` | Handles null/false gracefully |
| `"vc-plugin-class"` hardcoded | `cl("class")` via `classNameFactory` | Consistent, typo-proof |
| `console.log/warn` | Remove it | No logging in plugin code |
| `cdn.discordapp.com/avatars/...` | `IconUtils.getUserAvatarURL(user)` | Handles animated, sizing, CDN |
| `cdn.discordapp.com/icons/...` | `IconUtils.getGuildIconURL(...)` | Same |
| `cdn.discordapp.com/banners/...` | `IconUtils.getUserBannerURL(...)` | Same |
| `cdn.discordapp.com/emojis/...` | `IconUtils.getEmojiURL(...)` | Same |
| `/api/v9/...` or `/users/@me` | `Constants.Endpoints.*` or `RestAPI` | Endpoints change |
| `@api/Styles` for classNameFactory | `@utils/css` | `@api/Styles` is deprecated |
| `any` for Discord objects | Import from `@vencord/discord-types` | Type safety |
| `as unknown as` casting | Find the correct type | Unsafe |
| `React.memo()` | Remove it | Not needed |
| `React.cloneElement` / `React.isValidElement` / `React.Children` | Find another approach | Forbidden |
| `React.lazy(() => import(...))` | `LazyComponent` from `@utils/lazyReact` | Framework-integrated |
| Empty `catch {}` | Handle the error (toast, fallback, rethrow) | Silent failures |
| CSS-only plugins | Must have actual logic/patches | Not allowed |
| Commented-out dead code | Delete it | Git has history |
| `document.querySelector(...)` | Use webpack patches | DOM manipulation forbidden |
| `Vencord.Plugins.plugins["X"]` | `isPluginEnabled` + direct import | Proper interop |
| `plugin.started` check | `isPluginEnabled(plugin.name)` | Proper interop |
| Unexplained magic numbers | Named constants | Readability |
---
## TypeScript and Code Style
**Prefer:** `?.` optional chaining, `??` nullish coalescing, `const`, arrow functions, destructuring, template literals, object shorthand, array methods (`.map`, `.filter`, `.find`, `.some`).
**Style:** early returns over nested conditions, trust TypeScript inference (don't annotate obvious types), inline single-use variables, flat over nested, explicit over implicit.
**Less is more.** Less code with the same functionality is always better. KISS over clever.
**No comments.** Never add comments unless explicitly asked. However, preserve existing comments as they contain important context.
```typescript
// GOOD - early return, destructuring, inference, no comments
const getUser = (id: string) => {
const user = UserStore.getUser(id);
if (!user) return null;
const { username, discriminator } = user;
return `${username}#${discriminator}`;
};
// BAD - nested, verbose, unnecessary annotations
function getUser(id: string): string | null {
// Get the user from the store
const user = UserStore.getUser(id);
if (user) {
const username: string = user.username;
const discriminator: string = user.discriminator;
return username + "#" + discriminator;
} else {
return null;
}
}
```
Note: ESLint already enforces `prefer-const`, `prefer-destructuring`, `eqeqeq`, strict equality, unused imports, and import sorting. Focus review on patterns ESLint cannot catch: code philosophy, unnecessary complexity, missing early returns, and unnecessary comments.
---
## React
**Conditional rendering:** return `null`, never `undefined` or bare `return;`
**ErrorBoundary:** wrap any component that is used in patches (`$self.renderThing()`): `renderThing: ErrorBoundary.wrap((props) => { ... }, { noop: true })`
**useEffect cleanup:** always return cleanup when subscribing to events, timers, or resources:
```typescript
useEffect(() => {
const handler = () => { /* ... */ };
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
```
---
## Discord Types
Never use `any` for Discord objects. Import proper types:
```typescript
import { User, Channel, Guild, GuildMember, Message, Role } from "@vencord/discord-types";
```
Store types, component prop types, and action types are all available. Never use `as unknown as` casting as a workaround.
---
## CSS and Class Names
**Every plugin** must use `classNameFactory` from `@utils/css` for class names:
```typescript
import { classNameFactory } from "@utils/css";
const cl = classNameFactory("vc-my-plugin-");
cl("container") // "vc-my-plugin-container"
```
**Combining classes** from different sources: use `classes()` from `@utils/misc`:
```typescript
className={classes(cl("wrapper"), someDiscordClass, isActive && cl("active"))}
```
---
## Settings
- `settings.store.key` - reactive, auto-persists (React components)
- `settings.plain.key` - non-reactive raw value (basically never use this, only for special cases)
---
## Patch Quality
Patches modify Discord's minified webpack modules. Stability is paramount.
- **One patch per concern.** Each replacement does one thing.
- **Surgical.** Match only what needs replacing, let `find` target the module.
- **No hardcoded minified vars.** Never `e`, `t`, `n`, `r`, `i`, `eD`, `eH` etc. Use `\i`.
- **Bounded gaps.** `.{0,50}?` not `.+?` or `.*?` (unbounded = cross-match bugs).
- **No generic patterns.** `/className:\i/` alone is too broad. Add stable anchors.
- **No raw intl hashes.** Use `#{intl::KEY_NAME}` not `.aA4Vce`. If key is unknown, use `#{intl::aA4Vce::raw}`.
- **Capture groups only when reused** in replace via `$1`, `$2` (or as replace function parameter).
- **`$&`** for append/prepend, **`$self`** for plugin method calls.
```typescript
// GOOD
patches: [{
find: "#{intl::PIN_MESSAGE}),icon:",
replacement: {
match: /#{intl::PIN_MESSAGE}\)/,
replace: "$self.getPinLabel(arguments[0]))"
}
}]
// BAD
patches: [{
find: "pinMessage",
replacement: {
match: /label:e\.pinned\?.+?pinMessage\)/,
replace: "$self.getPinLabel(e))"
}
}]
```
---
## Plugin Interop
```typescript
// GOOD
import { isPluginEnabled } from "@api/PluginManager";
import otherPlugin from "@equicordplugins/otherPlugin";
if (!isPluginEnabled(otherPlugin.name)) return null;
otherPlugin.someFunction();
// BAD
Vencord.Plugins.plugins["OtherPlugin"].someFunction();
(somePlugin as unknown as { method(): void }).method();
```
---
## Text and Descriptions
- Plugin and setting descriptions: capital first letter.
- Error messages and toasts: natural human text, no dashes or robotic formatting.
```typescript
// GOOD
description: "Adds a button to copy message content."
showToast("Module not found");
// BAD
description: "adds a button to copy message content"
showToast("Module - not found");
```
---
## Performance
- `Map`/`Set` for frequent lookups, not arrays
- `.find()` / `.some()` not `.filter()[0]`
- `Promise.all()` for parallel async
- no local scope webpack `find*` functions (including start or stop methods), always find*Lazy on top level
---
## Memory Leaks and Cleanup
Plugins that don't use patches don't require a Discord restart to apply. Their `start()` and `stop()` must work cleanly so users can toggle them on/off without leaking resources.
**Every `start()` must have a matching `stop()`.** If `start()` subscribes, registers, or creates anything, `stop()` must undo it. Common leaks to flag:
- Event listeners added but never removed (FluxDispatcher, DOM events, MessageEvents)
- Intervals/timeouts set but never cleared
- Context menu patches added but never removed (use declarative `contextMenus` instead)
- Chat bar buttons added but never removed (use declarative `chatBarButton` instead)
- Styles enabled but never disabled (use `managedStyle` instead)
- Flux subscriptions without corresponding unsubscriptions
**Prefer declarative APIs** that handle cleanup automatically: `flux`, `contextMenus`, `chatBarButton`, `messagePopoverButton`, `managedStyle`. These don't need manual cleanup.
**useEffect must return cleanup** when it creates subscriptions, timers, observers, or any persistent resource. No exceptions.
**Watch for stale closures and references.** If a plugin stores references to DOM nodes, channels, or users in module scope, check that they're nulled out in `stop()`.
```typescript
// GOOD - clean start/stop, no leaks
start() {
this.interval = setInterval(this.update, 5000);
document.addEventListener("visibilitychange", this.onVisibilityChange);
},
stop() {
clearInterval(this.interval);
document.removeEventListener("visibilitychange", this.onVisibilityChange);
}
// BAD - leaks interval and listener forever
start() {
setInterval(this.update, 5000);
document.addEventListener("visibilitychange", this.onVisibilityChange);
},
stop() {}
```
---
## Code Hygiene
- **Delete dead code**, don't comment it out. Git preserves history.
- **No overengineering.** No premature abstractions, no "just in case" handling, no features not requested. Three similar lines beats a one-use helper. If code can be half the size and do the same thing, it should be.
- **No bloat.** Flag unnecessary wrapper functions, redundant type annotations, excessive error handling for things that can't fail, useless try/catch around synchronous code, or 50 lines doing what 10 could do. Code should be lean.
- **No unused imports.** Every import must be referenced.
- **No logging in plugin code besides for errors.** No `console.log/warn`.
---
## AI Slop Detection
If a PR shows signs of being AI-generated, mention it but **still review the code fully**. Do not reject or stop reviewing just because it looks AI-generated. Note it as an observation, then continue reviewing every issue as you normally would. Common AI slop patterns:
- Excessive comments explaining obvious code ("// get the user", "// check if null", "// return the result")
- Overly verbose variable names that read like sentences (`isUserCurrentlyLoggedInToTheApplication`)
- Unnecessary abstractions and wrapper functions for trivial operations
- "Just in case" error handling everywhere, including places that can never fail
- Reimplementing utilities that already exist in the codebase (check the Built-in Utilities section)
- Generic, boilerplate-heavy code that looks like it was prompted with "write a Discord plugin that..."
- Excessive JSDoc/TSDoc on every function, even internal one-liners
- Overly defensive coding: null checks on things that are never null, type guards on known types
- Cookie-cutter patterns repeated without understanding (e.g. wrapping every single line in try/catch)
- Descriptions and messages that sound robotic or templated rather than human-written
If you suspect it, say something like: "This looks like it might be AI-generated. There's a lot of unnecessary comments, over-engineering, and reimplemented utilities." Then proceed with the full review, flagging every concrete issue as usual.
---
## Built-in Utilities Reference
If a PR reimplements any of these, flag it. They already exist:
**@utils/misc:** `classes`, `sleep`, `isObject`, `isObjectEmpty`, `parseUrl`, `pluralise`, `identity`
**@utils/guards:** `isTruthy`, `isNonNullish`
**@utils/text:** `formatDuration`, `formatDurationMs`, `humanFriendlyJoin`, `makeCodeblock`, `toInlineCode`, `escapeRegExp`
**@utils/discord:** `getCurrentChannel`, `getCurrentGuild`, `getIntlMessage`, `openPrivateChannel`, `insertTextIntoChatInputBox`, `sendMessage`, `copyWithToast`, `openUserProfile`, `fetchUserProfile`, `getUniqueUsername`, `openInviteModal`
**@utils/css:** `classNameFactory`, `classNameToSelector`
**@utils/clipboard:** `copyToClipboard`
**@utils/modal:** `openModal`, `closeModal`, `ModalRoot`, `ModalHeader`, `ModalContent`, `ModalFooter`, `ModalCloseButton`
**@utils/margins:** `Margins.top8`, `.top16`, `.bottom8`, `.bottom16` etc.
**@utils/web:** `saveFile`, `chooseFile`
**@utils/lazy:** `proxyLazy`, `makeLazy`
**@utils/lazyReact:** `LazyComponent`
**@utils/react:** `useAwaiter`, `useForceUpdater`, `useTimer`
**@api/DataStore:** `get`, `set`, `del` (IndexedDB, async)
**@api/Commands:** `sendBotMessage`, `findOption`
**@webpack/common:**
- Stores: `UserStore`, `GuildStore`, `ChannelStore`, `GuildMemberStore`, `SelectedChannelStore`, `SelectedGuildStore`, `PresenceStore`, `RelationshipStore`, `MessageStore`, `EmojiStore`, `ThemeStore`, `PermissionStore`, `VoiceStateStore`, 30+ more
- Actions: `RestAPI`, `FluxDispatcher`, `MessageActions`, `NavigationRouter`, `ChannelRouter`, `ChannelActionCreators`, `SettingsRouter`
- Utils: `Constants` (`.Endpoints`), `SnowflakeUtils`, `Parser`, `PermissionsBits`, `moment`, `lodash`, `IconUtils`, `ColorUtils`, `ImageUtils`, `DateUtils`, `UsernameUtils`, `DisplayProfileUtils`
- Components: `Tooltip`, `TextInput`, `TextArea`, `Select`, `Slider`, `Avatar`, `Menu`, `Popout`, `ScrollerThin`, `Timestamp`, `MaskedLink`, `ColorPicker`
- Toasts: `Toasts`, `showToast`
- React: `useState`, `useEffect`, `useCallback`, `useStateFromStores`
**@webpack finders:** `findByPropsLazy`, `findByCodeLazy`, `findStoreLazy`, `findComponentByCodeLazy`, `findExportedComponentLazy`
**@components/:** `ErrorBoundary`, `Flex`, `Button`, `Paragraph`, `Heading`, `BaseText`, `Span`, `ErrorCard`, `Link`, `CodeBlock`, `FormSwitch`
---
## Review Severity
1. **CRITICAL** - `any` types, hardcoded CDN/API URLs, direct DOM manipulation, security issues, massive overengineering/bloat, suspected AI slop (note it, but still review everything)
2. **HIGH** - Missing useEffect cleanup, forbidden React APIs, hardcoded class names, console logging statements, missing definePluginSettings, unnecessary abstractions
3. **MEDIUM** - Anti-patterns (`||` instead of `??`), utility reimplementations, commented-out code, bad description format, excessive comments
4. **LOW** - Style preferences, minor performance wins, organization

View file

@ -1,9 +1,13 @@
<!--
We do not accept PRs that were created by AI. You will be permanently blocked with no further warning if you submit AI generated PRs.
Also do not use AI in communication, it makes me ill
-->
## Describe your Changes
## Screenshots (if applicable)
## Checklist before submitting
<!-- Hint: [x] this is how to check boxes -->
- [ ] I have read the [CONTRIBUTING.md](./CONTRIBUTING.md) file and made sure this pull request complies with it
- [ ] This pull request was written by me, and not an AI agent

View file

@ -1,7 +1,7 @@
{
"name": "equicord",
"private": "true",
"version": "1.15.0.4",
"version": "1.15.1.0",
"description": "The other cutest Discord client mod",
"homepage": "https://github.com/Equicord/Equicord#readme",
"bugs": {

View file

@ -34,6 +34,11 @@ export interface ChannelMessages {
_before: MessageCache;
_after: MessageCache;
_map: Record<string, Message>;
// has a ton more undocumented methods
some(predicate: (message: Message, index: number, array: Message[]) => unknown, thisArg?: any): boolean;
forEach(callback: (message: Message, index: number, array: Message[]) => void, thisArg?: any): void;
receiveMessage(msg: any): this;
get(msgId: string): Message | undefined;
}
export class MessageStore extends FluxStore {

View file

@ -256,16 +256,17 @@ export interface ExpressionPickerStoreState extends Record<PropertyKey, any> {
activeView: ActiveView | null;
lastActiveView: ActiveView | null;
activeViewType: any | null;
activeChannelId: string | null;
searchQuery: string;
isSearchSuggestion: boolean,
pickerId: string;
}
export interface ExpressionPickerStore {
openExpressionPicker(activeView: ActiveView, activeViewType?: any): void;
closeExpressionPicker(activeViewType?: any): void;
toggleMultiExpressionPicker(activeViewType?: any): void;
toggleExpressionPicker(activeView: ActiveView, activeViewType?: any): void;
openExpressionPicker(activeView: ActiveView, activeViewType?: any, channelId?: string): void;
closeExpressionPicker(activeViewType?: any, channelId?: string): void;
toggleMultiExpressionPicker(activeViewType?: any, channelId?: string): void;
toggleExpressionPicker(activeView: ActiveView, activeViewType?: any, channelId?: string): void;
setExpressionPickerView(activeView: ActiveView): void;
setSearchQuery(searchQuery: string, isSearchSuggestion?: boolean): void;
useExpressionPickerStore(): ExpressionPickerStoreState;

View file

@ -24,7 +24,7 @@ import { Flex } from "@components/Flex";
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { Devs } from "@utils/constants";
import { getGuildAcronym } from "@utils/discord";
import { getGuildAcronym, hasGuildFeature } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types";
import { Guild, GuildSticker } from "@vencord/discord-types";
@ -35,8 +35,6 @@ import { Promisable } from "type-fest";
const uploadEmoji = findByCodeLazy(".GUILD_EMOJIS(", "EMOJI_UPLOAD_START");
const getGuildMaxEmojiSlots = findByCodeLazy(".additionalEmojiSlots") as (guild: Guild) => number;
interface Sticker extends GuildSticker {
t: "Sticker";
}
@ -74,6 +72,13 @@ function getGuildMaxStickerSlots(guild: Guild) {
return PremiumTierStickerLimitMap[guild.premiumTier] ?? PremiumTierStickerLimitMap[0];
}
function getGuildMaxEmojiSlots(guild: Guild) {
return Math.max(
hasGuildFeature(guild, "MORE_EMOJI") ? 200 : 50,
50 + (guild.premiumFeatures?.additionalEmojiSlots ?? 0)
);
}
function getUrl(data: Data, size: number) {
if (data.t === "Emoji")
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.webp?size=${size}&lossless=true&animated=true`;

View file

@ -145,7 +145,7 @@ async function fetchMessage(channelID: string, messageID: string) {
const msg = res?.body?.[0];
if (!msg) return;
const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id);
const message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id);
if (!message) return;
messageCache.set(message.id, {

View file

@ -75,7 +75,7 @@ function clearMessageHistory(msg: MLMessage) {
FluxDispatcher.dispatch({
type: "MESSAGE_DELETE",
channelId: msg.channel_id,
messageId: msg.id,
id: msg.id,
mlDeleted: true
});
} else {
@ -173,7 +173,7 @@ const patchChannelContextMenu: NavContextMenuPatchCallback = (
children,
{ channel },
) => {
const messages = MessageStore.getMessages(channel?.id) as MLMessage[];
const messages = MessageStore.getMessages(channel?.id);
if (!messages?.some(msg => doesMessageHaveHistory(msg))) return;
const group = findGroupChildrenByChildId("mark-channel-read", children) ?? children;

View file

@ -16,8 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { isPluginEnabled } from "@api/PluginManager";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import NoBlockedMessagesPlugin from "@plugins/noBlockedMessages";
import { Devs } from "@utils/constants";
import { sleep } from "@utils/misc";
import { Queue } from "@utils/Queue";
@ -87,28 +89,13 @@ function handleClickAvatar(event: React.UIEvent<HTMLElement, Event>) {
event.stopPropagation();
}
function ReactionUsers({ message, emoji, type }: ReactionProps) {
const forceUpdate = useForceUpdater();
function ReactionUsers({ message, users }: { message: Message, users: User[]; }) {
useLayoutEffect(() => { // bc need to prevent autoscrolling
if (Scroll?.scrollCounter > 0) {
Scroll.setAutomaticAnchor(null);
}
});
useEffect(() => {
const cb = (e: any) => {
if (e?.messageId === message.id)
forceUpdate();
};
FluxDispatcher.subscribe("MESSAGE_REACTION_ADD_USERS", cb);
return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb);
}, [message.id, forceUpdate]);
const reactions = getReactionsWithQueue(message, emoji, type);
const users = Array.from(reactions, ([id]) => UserStore.getUser(id)).filter(Boolean);
return (
<div
style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}
@ -144,7 +131,7 @@ export default definePlugin({
name: "WhoReacted",
description: "Renders the avatars of users who reacted to a message",
tags: ["Reactions", "Chat", "Appearance"],
authors: [Devs.Ven, Devs.KannaDev, Devs.newwares],
authors: [Devs.Ven, Devs.KannaDev, Devs.newwares, Devs.paige],
isModified: true,
settings,
patches: [
@ -172,10 +159,33 @@ export default definePlugin({
}
],
renderUsers: ErrorBoundary.wrap((props: ReactionProps) => {
return props.message.reactions.length > 10
renderUsers: ErrorBoundary.wrap(({ message, emoji, type }: ReactionProps) => {
const forceUpdate = useForceUpdater();
useEffect(() => {
const cb = (e: any) => {
if (e?.messageId === message.id)
forceUpdate();
};
FluxDispatcher.subscribe("MESSAGE_REACTION_ADD_USERS", cb);
return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb);
}, [message.id, forceUpdate]);
if (message.reactions.length > 10) return null;
const reactionMap = getReactionsWithQueue(message, emoji, type);
let users = Array.from(reactionMap, ([id]) => UserStore.getUser(id)).filter(Boolean);
if (isPluginEnabled(NoBlockedMessagesPlugin.name))
users = users.filter(user => {
const { blocked, ignored } = NoBlockedMessagesPlugin.getRelationshipStatus(user);
return !(blocked || (ignored && NoBlockedMessagesPlugin.settings.store.alsoHideIgnoredUsers));
});
return users.length === 0
? null
: <ReactionUsers {...props} />;
: <ReactionUsers message={message} users={users} />;
}, { noop: true }),
setScrollObj(scroll: any) {

View file

@ -670,6 +670,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Lunascape",
id: 383365021415243776n
},
paige: {
name: "paige",
id: 1375697625864601650n
},
jax: {
name: "jax",
id: 1493703027801194598n

View file

@ -452,3 +452,9 @@ export type PluginNative<PluginExports extends Record<string, (event: Electron.I
};
export type AllOrNothing<T> = T | { [K in keyof T]?: never; };
export type ConstEnumToRuntimeEnum<T> = {
[K in keyof T as T[K] extends number ? T[K] : never]: K;
} & {
[K in keyof T]: T[K];
};

View file

@ -16,7 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ConstEnumToRuntimeEnum } from "@utils/types";
import * as t from "@vencord/discord-types";
import * as enums from "@vencord/discord-types/enums";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { waitForStore } from "./internal";
@ -25,11 +27,9 @@ export const Flux: t.Flux = findByPropsLazy("connectStores");
export type GenericStore = t.FluxStore & Record<string, any>;
export const DraftType = findByPropsLazy("ChannelMessage", "SlashCommand");
export const DraftType: ConstEnumToRuntimeEnum<typeof enums.DraftType> = findByPropsLazy("ChannelMessage", "SlashCommand");
export let MessageStore: Omit<t.MessageStore, "getMessages"> & GenericStore & {
getMessages(chanId: string): any;
};
export let MessageStore: t.MessageStore;
export let PermissionStore: t.PermissionStore;
export let GuildChannelStore: t.GuildChannelStore;