Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
94c0ac8e16
13 changed files with 70 additions and 422 deletions
|
|
@ -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"
|
|
||||||
|
|
@ -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
|
|
||||||
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
|
|
@ -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.
|
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
|
## Describe your Changes
|
||||||
|
|
||||||
|
## Screenshots (if applicable)
|
||||||
|
|
||||||
## Checklist before submitting
|
## 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
|
- [ ] 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
|
- [ ] This pull request was written by me, and not an AI agent
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "equicord",
|
"name": "equicord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.15.0.4",
|
"version": "1.15.1.0",
|
||||||
"description": "The other cutest Discord client mod",
|
"description": "The other cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Equicord/Equicord#readme",
|
"homepage": "https://github.com/Equicord/Equicord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@ export interface ChannelMessages {
|
||||||
_before: MessageCache;
|
_before: MessageCache;
|
||||||
_after: MessageCache;
|
_after: MessageCache;
|
||||||
_map: Record<string, Message>;
|
_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 {
|
export class MessageStore extends FluxStore {
|
||||||
|
|
|
||||||
9
packages/discord-types/src/utils.d.ts
vendored
9
packages/discord-types/src/utils.d.ts
vendored
|
|
@ -256,16 +256,17 @@ export interface ExpressionPickerStoreState extends Record<PropertyKey, any> {
|
||||||
activeView: ActiveView | null;
|
activeView: ActiveView | null;
|
||||||
lastActiveView: ActiveView | null;
|
lastActiveView: ActiveView | null;
|
||||||
activeViewType: any | null;
|
activeViewType: any | null;
|
||||||
|
activeChannelId: string | null;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
isSearchSuggestion: boolean,
|
isSearchSuggestion: boolean,
|
||||||
pickerId: string;
|
pickerId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExpressionPickerStore {
|
export interface ExpressionPickerStore {
|
||||||
openExpressionPicker(activeView: ActiveView, activeViewType?: any): void;
|
openExpressionPicker(activeView: ActiveView, activeViewType?: any, channelId?: string): void;
|
||||||
closeExpressionPicker(activeViewType?: any): void;
|
closeExpressionPicker(activeViewType?: any, channelId?: string): void;
|
||||||
toggleMultiExpressionPicker(activeViewType?: any): void;
|
toggleMultiExpressionPicker(activeViewType?: any, channelId?: string): void;
|
||||||
toggleExpressionPicker(activeView: ActiveView, activeViewType?: any): void;
|
toggleExpressionPicker(activeView: ActiveView, activeViewType?: any, channelId?: string): void;
|
||||||
setExpressionPickerView(activeView: ActiveView): void;
|
setExpressionPickerView(activeView: ActiveView): void;
|
||||||
setSearchQuery(searchQuery: string, isSearchSuggestion?: boolean): void;
|
setSearchQuery(searchQuery: string, isSearchSuggestion?: boolean): void;
|
||||||
useExpressionPickerStore(): ExpressionPickerStoreState;
|
useExpressionPickerStore(): ExpressionPickerStoreState;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { Flex } from "@components/Flex";
|
||||||
import { Heading } from "@components/Heading";
|
import { Heading } from "@components/Heading";
|
||||||
import { Paragraph } from "@components/Paragraph";
|
import { Paragraph } from "@components/Paragraph";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { getGuildAcronym } from "@utils/discord";
|
import { getGuildAcronym, hasGuildFeature } from "@utils/discord";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Guild, GuildSticker } from "@vencord/discord-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 uploadEmoji = findByCodeLazy(".GUILD_EMOJIS(", "EMOJI_UPLOAD_START");
|
||||||
|
|
||||||
const getGuildMaxEmojiSlots = findByCodeLazy(".additionalEmojiSlots") as (guild: Guild) => number;
|
|
||||||
|
|
||||||
interface Sticker extends GuildSticker {
|
interface Sticker extends GuildSticker {
|
||||||
t: "Sticker";
|
t: "Sticker";
|
||||||
}
|
}
|
||||||
|
|
@ -74,6 +72,13 @@ function getGuildMaxStickerSlots(guild: Guild) {
|
||||||
return PremiumTierStickerLimitMap[guild.premiumTier] ?? PremiumTierStickerLimitMap[0];
|
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) {
|
function getUrl(data: Data, size: number) {
|
||||||
if (data.t === "Emoji")
|
if (data.t === "Emoji")
|
||||||
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.webp?size=${size}&lossless=true&animated=true`;
|
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.webp?size=${size}&lossless=true&animated=true`;
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ async function fetchMessage(channelID: string, messageID: string) {
|
||||||
const msg = res?.body?.[0];
|
const msg = res?.body?.[0];
|
||||||
if (!msg) return;
|
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;
|
if (!message) return;
|
||||||
|
|
||||||
messageCache.set(message.id, {
|
messageCache.set(message.id, {
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ function clearMessageHistory(msg: MLMessage) {
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
type: "MESSAGE_DELETE",
|
type: "MESSAGE_DELETE",
|
||||||
channelId: msg.channel_id,
|
channelId: msg.channel_id,
|
||||||
messageId: msg.id,
|
id: msg.id,
|
||||||
mlDeleted: true
|
mlDeleted: true
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -173,7 +173,7 @@ const patchChannelContextMenu: NavContextMenuPatchCallback = (
|
||||||
children,
|
children,
|
||||||
{ channel },
|
{ channel },
|
||||||
) => {
|
) => {
|
||||||
const messages = MessageStore.getMessages(channel?.id) as MLMessage[];
|
const messages = MessageStore.getMessages(channel?.id);
|
||||||
if (!messages?.some(msg => doesMessageHaveHistory(msg))) return;
|
if (!messages?.some(msg => doesMessageHaveHistory(msg))) return;
|
||||||
|
|
||||||
const group = findGroupChildrenByChildId("mark-channel-read", children) ?? children;
|
const group = findGroupChildrenByChildId("mark-channel-read", children) ?? children;
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { isPluginEnabled } from "@api/PluginManager";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import NoBlockedMessagesPlugin from "@plugins/noBlockedMessages";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { sleep } from "@utils/misc";
|
import { sleep } from "@utils/misc";
|
||||||
import { Queue } from "@utils/Queue";
|
import { Queue } from "@utils/Queue";
|
||||||
|
|
@ -87,28 +89,13 @@ function handleClickAvatar(event: React.UIEvent<HTMLElement, Event>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReactionUsers({ message, emoji, type }: ReactionProps) {
|
function ReactionUsers({ message, users }: { message: Message, users: User[]; }) {
|
||||||
const forceUpdate = useForceUpdater();
|
|
||||||
|
|
||||||
useLayoutEffect(() => { // bc need to prevent autoscrolling
|
useLayoutEffect(() => { // bc need to prevent autoscrolling
|
||||||
if (Scroll?.scrollCounter > 0) {
|
if (Scroll?.scrollCounter > 0) {
|
||||||
Scroll.setAutomaticAnchor(null);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}
|
style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}
|
||||||
|
|
@ -144,7 +131,7 @@ export default definePlugin({
|
||||||
name: "WhoReacted",
|
name: "WhoReacted",
|
||||||
description: "Renders the avatars of users who reacted to a message",
|
description: "Renders the avatars of users who reacted to a message",
|
||||||
tags: ["Reactions", "Chat", "Appearance"],
|
tags: ["Reactions", "Chat", "Appearance"],
|
||||||
authors: [Devs.Ven, Devs.KannaDev, Devs.newwares],
|
authors: [Devs.Ven, Devs.KannaDev, Devs.newwares, Devs.paige],
|
||||||
isModified: true,
|
isModified: true,
|
||||||
settings,
|
settings,
|
||||||
patches: [
|
patches: [
|
||||||
|
|
@ -172,10 +159,33 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
renderUsers: ErrorBoundary.wrap((props: ReactionProps) => {
|
renderUsers: ErrorBoundary.wrap(({ message, emoji, type }: ReactionProps) => {
|
||||||
return props.message.reactions.length > 10
|
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
|
? null
|
||||||
: <ReactionUsers {...props} />;
|
: <ReactionUsers message={message} users={users} />;
|
||||||
}, { noop: true }),
|
}, { noop: true }),
|
||||||
|
|
||||||
setScrollObj(scroll: any) {
|
setScrollObj(scroll: any) {
|
||||||
|
|
|
||||||
|
|
@ -670,6 +670,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "Lunascape",
|
name: "Lunascape",
|
||||||
id: 383365021415243776n
|
id: 383365021415243776n
|
||||||
},
|
},
|
||||||
|
paige: {
|
||||||
|
name: "paige",
|
||||||
|
id: 1375697625864601650n
|
||||||
|
},
|
||||||
jax: {
|
jax: {
|
||||||
name: "jax",
|
name: "jax",
|
||||||
id: 1493703027801194598n
|
id: 1493703027801194598n
|
||||||
|
|
|
||||||
|
|
@ -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 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];
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 { ConstEnumToRuntimeEnum } from "@utils/types";
|
||||||
import * as t from "@vencord/discord-types";
|
import * as t from "@vencord/discord-types";
|
||||||
|
import * as enums from "@vencord/discord-types/enums";
|
||||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||||
|
|
||||||
import { waitForStore } from "./internal";
|
import { waitForStore } from "./internal";
|
||||||
|
|
@ -25,11 +27,9 @@ export const Flux: t.Flux = findByPropsLazy("connectStores");
|
||||||
|
|
||||||
export type GenericStore = t.FluxStore & Record<string, any>;
|
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 & {
|
export let MessageStore: t.MessageStore;
|
||||||
getMessages(chanId: string): any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export let PermissionStore: t.PermissionStore;
|
export let PermissionStore: t.PermissionStore;
|
||||||
export let GuildChannelStore: t.GuildChannelStore;
|
export let GuildChannelStore: t.GuildChannelStore;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue