Merge branch 'dev'
This commit is contained in:
commit
22592b1435
22 changed files with 935 additions and 478 deletions
|
|
@ -1,12 +1,22 @@
|
||||||
have_fun: false
|
|
||||||
memory_config:
|
|
||||||
disabled: false
|
|
||||||
code_review:
|
code_review:
|
||||||
disable: false
|
disable: false
|
||||||
comment_severity_threshold: LOW
|
comment_severity_threshold: MEDIUM
|
||||||
max_review_comments: -1
|
max_review_comments: -1
|
||||||
pull_request_opened:
|
pull_request_opened:
|
||||||
help: false
|
help: false
|
||||||
summary: false
|
summary: false
|
||||||
code_review: false
|
code_review: false
|
||||||
include_drafts: false
|
include_drafts: false
|
||||||
|
|
||||||
|
ignore_patterns:
|
||||||
|
- "pnpm-lock.yaml"
|
||||||
|
- "pnpm-workspace.yaml"
|
||||||
|
- "dist/**"
|
||||||
|
- "browser/**"
|
||||||
|
- "packages/**"
|
||||||
|
- "patches/**"
|
||||||
|
- "misc/**"
|
||||||
|
- "scripts/**"
|
||||||
|
- ".vscode/**"
|
||||||
|
- ".github/**"
|
||||||
|
- "*.map"
|
||||||
|
|
|
||||||
|
|
@ -1,134 +1,382 @@
|
||||||
title: Equicord Review Guidelines
|
# Equicord Code Review Style Guide
|
||||||
|
|
||||||
rules:
|
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.
|
||||||
critical:
|
|
||||||
licenseHeader: NEVER edit existing license headers, new files use 2026
|
|
||||||
checkDeprecated: always verify components/functions arent deprecated before using
|
|
||||||
noAnyTypes: NEVER use any for Discord objects, import proper types from @vencord/discord-types
|
|
||||||
minimalChanges: fix with smallest possible change, no refactoring unless explicitly asked
|
|
||||||
noOverengineering: solve the problem at hand only, no patterns for patterns sake
|
|
||||||
humanText: write natural human text in errors, descriptions, messages. No dashes or robotic formatting. Write like "Module not found" not "Module - not found". Keep it simple and conversational
|
|
||||||
code:
|
|
||||||
deleteNotComment: DELETE dead code, dont comment it, git preserves history
|
|
||||||
noHardcoded: no hardcoded values, unused imports, magic numbers
|
|
||||||
useClassNameFactory: import classNameFactory from @utils/css (NOT @api/Styles which is deprecated), ALWAYS use cl() for class names, never hardcode strings like "vc-plugin-name-class"
|
|
||||||
useClasses: when combining multiple class names use classes() from @utils/misc, not template strings like `${a} ${b}`
|
|
||||||
useExisting: use utilities from @utils/, @api/, @components/
|
|
||||||
logger: use Logger from @utils/Logger, not console.log
|
|
||||||
descriptions: capital first letter, end with period
|
|
||||||
minimal: less code with same functionality is ALWAYS better
|
|
||||||
noComments: NEVER add comments unless explicitly asked
|
|
||||||
preserveComments: keep existing comments, they contain important context
|
|
||||||
performance: use Map/Set, .some/.find, no spread in loops, Promise.all
|
|
||||||
simple: KISS over clever, flat over nested, explicit over implicit
|
|
||||||
typescript:
|
|
||||||
prefer: optional chaining (?.), nullish coalescing (??), const, arrow functions
|
|
||||||
use: destructuring, template literals, object shorthand, array methods
|
|
||||||
style: early returns, trust inference, inline single-use variables
|
|
||||||
react:
|
|
||||||
errorBoundary: wrap complex components with ErrorBoundary.wrap(Component, { noop: true })
|
|
||||||
nullNotUndefined: return null for conditional rendering not undefined
|
|
||||||
forbidden: React.cloneElement, React.isValidElement, React.memo(), React.lazy, React.Children
|
|
||||||
cleanup: always return cleanup functions in useEffect
|
|
||||||
forbidden:
|
|
||||||
DOM: direct DOM manipulation (use patches)
|
|
||||||
cssOnly: CSS-only plugins
|
|
||||||
emptyCatch: empty catch blocks
|
|
||||||
hardcodedVars: hardcoded minified vars in patches (e,t,n)
|
|
||||||
settingsArrays: NEVER use settings.use() with arrays, mutate then reassign
|
|
||||||
antiPatterns{bad ⟹ good}:
|
|
||||||
"value !== null && value !== undefined" ⟹ "value"
|
|
||||||
"array && array.length > 0" ⟹ "array.length"
|
|
||||||
"settings?.store?.value" ⟹ "settings.store.value"
|
|
||||||
"value || defaultValue" ⟹ "value ?? defaultValue"
|
|
||||||
|
|
||||||
patching:
|
---
|
||||||
core:
|
|
||||||
surgical: minimum code touched, stability over cleverness, one patch per concern
|
|
||||||
iterate: try multiple approaches, keep simplifying until you find the cleanest solution
|
|
||||||
minimize: remove any regex character that doesnt change the match result
|
|
||||||
balanced: not overengineered, not overlooked, just clean and future-proof
|
|
||||||
find:
|
|
||||||
preferIntl: use #{intl::KEY} in find when possible, most stable anchor
|
|
||||||
makeUnique: if find matches multiple modules, add nearby stable string like "),icon:" or function name
|
|
||||||
combine: "#{intl::PIN_MESSAGE}),icon:" is better than generic strings
|
|
||||||
match:
|
|
||||||
shortestPath: match only what you need to replace, let find do the targeting
|
|
||||||
intlInMatch: can use #{intl::KEY} in match too, gets canonicalized to hash
|
|
||||||
simpleWins: /#{intl::KEY}\)/ beats /label:\i\.pinned\?.{0,60}#{intl::KEY}\)/
|
|
||||||
useAmpersand: $& keeps original, append/prepend to it
|
|
||||||
boundedGaps: .{0,50} not .+? or .*?
|
|
||||||
captureOnlyIfNeeded: only use capture groups if reusing in replace
|
|
||||||
NEVER:
|
|
||||||
hardcodedMinified: e,t,n,r,i,o,s,l,c,u,$_,xx,eD,eH,eW ⟹ use \i instead
|
|
||||||
minifiedChains: \i\.\i alone ⟹ surround with stable strings
|
|
||||||
unboundedGaps: .+? or .*? ⟹ use .{0,N}
|
|
||||||
genericPatterns: /className:\i/ alone ⟹ add anchor
|
|
||||||
rawIntlHash: .aA4Vce ⟹ #{intl::KEY_NAME}
|
|
||||||
examples:
|
|
||||||
clean: |
|
|
||||||
find: "#{intl::PIN_MESSAGE}),icon:"
|
|
||||||
match: /#{intl::PIN_MESSAGE}\)/
|
|
||||||
replace: "$self.getPinLabel(arguments[0]))"
|
|
||||||
append: |
|
|
||||||
find: "#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}"
|
|
||||||
match: /#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}.{0,100}(?=])/
|
|
||||||
replace: "$&,$self.renderTooltip(arguments[0].guild)"
|
|
||||||
wrap: |
|
|
||||||
find: "#{intl::THREE_USERS_TYPING}"
|
|
||||||
match: /(?<="aria-atomic":!0,children:)\i/
|
|
||||||
replace: "$self.renderTypingUsers({ children: $& })"
|
|
||||||
|
|
||||||
intl:
|
## Plugin Acceptance (Instant Reject)
|
||||||
syntax: "#{intl::KEY}" or "#{intl::HASH::raw}" if key unknown
|
|
||||||
workflow: hash in code → reverse to get key → use #{intl::KEY} or fallback to #{intl::HASH::raw}
|
|
||||||
|
|
||||||
pluginInterop:
|
If a plugin breaks ANY of these, reject the entire PR. These are non-negotiable:
|
||||||
checkEnabled: isPluginEnabled(name) from @api/PluginManager takes a string, checks required/isDependency/enabled
|
|
||||||
import: import the plugin directly from its path to access .name and functions
|
|
||||||
avoid: dont use Vencord.Plugins.plugins or plugin.started, dont use "as unknown as" casting
|
|
||||||
example: |
|
|
||||||
import { isPluginEnabled } from "@api/PluginManager";
|
|
||||||
import otherPlugin from "@equicordplugins/otherPlugin";
|
|
||||||
if (!isPluginEnabled(otherPlugin.name)) return null;
|
|
||||||
otherPlugin.someFunction();
|
|
||||||
|
|
||||||
reference:
|
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.
|
||||||
types: Channel, Guild, GuildMember, User, Role, Message from @vencord/discord-types
|
2. **No simple text replacement plugins.** The built-in TextReplace plugin already covers this.
|
||||||
components: Paragraph, BaseText, Flex, Button, ErrorCard from @components/
|
3. **No raw DOM manipulation.** Always use patches and React.
|
||||||
settings: use definePluginSettings from @api/Settings, not inline settings object
|
4. **No FakeDeafen or FakeMute.**
|
||||||
utilities:
|
5. **No StereoMic-related plugins.**
|
||||||
clipboard: copyToClipboard from @utils/clipboard
|
6. **No UI-only hide/redesign plugins.** Use CSS for that. (Negotiable in rare cases.)
|
||||||
discord: insertTextIntoChatInputBox, getCurrentChannel, getCurrentGuild, getIntlMessage, openUserProfile, openPrivateChannel, sendMessage, copyWithToast, getUniqueUsername, fetchUserProfile from @utils/discord
|
7. **No plugins targeting specific third-party Discord bots.** Official Discord apps are fine.
|
||||||
css: classNameFactory, classNameToSelector from @utils/css
|
8. **No selfbot or API abuse.** Auto-replies, animated statuses, message pruning, Nitro snipers, etc.
|
||||||
misc: classes, sleep, isObject, isObjectEmpty, pluralise, parseUrl, identity from @utils/misc
|
9. **No untrusted third-party APIs.** Well-known services (Google, GitHub) are acceptable.
|
||||||
text: formatDuration, formatDurationMs, humanFriendlyJoin, makeCodeblock, toInlineCode, escapeRegExp from @utils/text
|
10. **No plugins requiring users to provide their own API keys.**
|
||||||
modal: openModal, closeModal, ModalRoot, ModalHeader, ModalContent, ModalCloseButton from @utils/modal
|
11. **No new dependencies** unless strictly necessary and well justified.
|
||||||
margins: Margins from @utils/margins (Margins.top8, Margins.bottom16, etc.)
|
|
||||||
guards: isTruthy, isNonNullish from @utils/guards
|
---
|
||||||
web: saveFile, chooseFile from @utils/web
|
|
||||||
webpackCommon:
|
## Plugin Structure
|
||||||
icons: IconUtils from @webpack/common, NEVER hardcode cdn.discordapp.com URLs
|
|
||||||
getUserAvatarURL: IconUtils.getUserAvatarURL(user, canAnimate?, size?)
|
Default export via `definePlugin` from `@utils/types`. Non-negotiable.
|
||||||
getDefaultAvatarURL: IconUtils.getDefaultAvatarURL(id)
|
|
||||||
getUserBannerURL: IconUtils.getUserBannerURL({ id, banner, canAnimate?, size })
|
```typescript
|
||||||
getGuildIconURL: IconUtils.getGuildIconURL({ id, icon, size?, canAnimate? })
|
import definePlugin from "@utils/types";
|
||||||
getGuildBannerURL: IconUtils.getGuildBannerURL(guild, canAnimate?)
|
import { EquicordDevs } from "@utils/constants";
|
||||||
getChannelIconURL: IconUtils.getChannelIconURL({ id, icon })
|
|
||||||
getEmojiURL: IconUtils.getEmojiURL({ id, animated, size })
|
export default definePlugin({
|
||||||
getApplicationIconURL: IconUtils.getApplicationIconURL(data)
|
name: "PluginName", // PascalCase, matches directory name
|
||||||
getGameAssetURL: IconUtils.getGameAssetURL(data)
|
description: "Does something.", // Capital first, period at end
|
||||||
stores: UserStore, GuildStore, ChannelStore, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, PresenceStore, RelationshipStore, MessageStore, EmojiStore, ThemeStore, PermissionStore, VoiceStateStore from @webpack/common
|
authors: [EquicordDevs.Name], // EquicordDevs for new, Devs for upstream
|
||||||
actions: RestAPI, FluxDispatcher, MessageActions, NavigationRouter, ChannelRouter, ChannelActionCreators, SettingsRouter from @webpack/common
|
});
|
||||||
utils: Constants (Constants.Endpoints), SnowflakeUtils, Parser, PermissionsBits, moment, lodash, ColorUtils, ImageUtils, DateUtils, UsernameUtils, DisplayProfileUtils from @webpack/common
|
```
|
||||||
components: Tooltip, TextInput, TextArea, Select, Slider, Avatar, Menu, Popout, ScrollerThin, Timestamp from @webpack/common
|
|
||||||
toasts: Toasts, showToast from @webpack/common
|
**Settings** must use `definePluginSettings` from `@api/Settings`. Reject inline `options` objects.
|
||||||
imports:
|
|
||||||
lazy: findByPropsLazy, findStoreLazy, findComponentByCodeLazy, findExportedComponentLazy from @webpack
|
```typescript
|
||||||
common: useState, useEffect, useCallback, useStateFromStores from @webpack/common
|
// GOOD
|
||||||
neverHardcode:
|
const settings = definePluginSettings({
|
||||||
cdnURLs: NEVER use "cdn.discordapp.com/avatars/", "cdn.discordapp.com/banners/", "cdn.discordapp.com/emojis/", "cdn.discordapp.com/icons/", "cdn.discordapp.com/channel-icons/" ⟹ use IconUtils
|
myOption: { type: OptionType.BOOLEAN, description: "Enables the feature.", default: true }
|
||||||
apiEndpoints: NEVER hardcode "/api/v9", "/users/@me" ⟹ use Constants.Endpoints or RestAPI
|
});
|
||||||
console: NEVER use console.log/warn/error ⟹ use Logger from @utils/Logger
|
export default definePlugin({ settings, /* ... */ });
|
||||||
classConcat: NEVER use `${a} ${b}` for class names ⟹ use classes(a, b) from @utils/misc
|
|
||||||
defaultAvatars: NEVER hardcode "/assets/*.png" for default avatars ⟹ use IconUtils.getDefaultAvatarURL(id)
|
// BAD
|
||||||
|
export default definePlugin({ options: { myOption: { /* ... */ } } });
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
**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 `isNonNullish(value)` | 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/error` | Remove it | No logging in plugin code |
|
||||||
|
| `new Logger(...)` / `logger.log(...)` | 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 |
|
||||||
|
| `/assets/*.png` default avatars | `IconUtils.getDefaultAvatarURL(id)` | 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 |
|
||||||
|
| Unused imports | Remove them | Cleanliness |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 complex components: `ErrorBoundary.wrap(MyComponent, { 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 (performance-critical code)
|
||||||
|
- **Never** `settings.use()` with arrays in variables. Mutate then reassign instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
- **Capture groups only when reused** in replace via `$1`, `$2`.
|
||||||
|
- **`$&`** 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, end with period.
|
||||||
|
- 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]`
|
||||||
|
- No spread `...` in loops
|
||||||
|
- `Promise.all()` for parallel async
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- MutationObservers created but never disconnected
|
||||||
|
- 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.** No `console.log/warn/error`, no `Logger`. Remove all logging statements.
|
||||||
|
- **No empty catch blocks.** Handle every error (show toast, return fallback, rethrow, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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, logging statements (console or Logger), 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
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export default definePlugin({
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=className:(\i)\.\i,style:\i,)children:\[/,
|
match: /(?<=className:(\i)\.\i,style:\i,)children:\[/,
|
||||||
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// fix discord weird shrink with extra buttons
|
// fix discord weird shrink with extra buttons
|
||||||
{
|
{
|
||||||
match: /(?<=\{ref:\i,)style:(\i)(?=,onMouseEnter:\i,onMouseDown:\i,onClick)/,
|
match: /(?<=\{ref:\i,)style:(\i)/,
|
||||||
replace: "style:{...$1,minWidth:0}"
|
replace: "style:{...$1,minWidth:0}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -54,76 +54,77 @@ export default definePlugin({
|
||||||
settings,
|
settings,
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||||
replacement: {
|
|
||||||
match: /,nameplate:\i,selectedGuildId:(\i),avatarDecoration/,
|
|
||||||
replace: ",nameplate:null,selectedGuildId:$1,avatarDecoration"
|
|
||||||
},
|
|
||||||
predicate: () => settings.store.removeNameplate
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=#{intl::MUTE}\),)className:\i\.\i,/,
|
match: /,nameplate:\i,selectedGuildId:(\i),avatarDecoration/,
|
||||||
replace: ""
|
replace: ",nameplate:null,selectedGuildId:$1,avatarDecoration",
|
||||||
|
predicate: () => settings.store.removeNameplate
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=#{intl::DEAFEN}\),)className:\i\.\i,/,
|
match: /hoverText:(\i),forceHover:\i,children:/g,
|
||||||
replace: ""
|
replace: "hoverText:$1,forceHover:!0,children:",
|
||||||
|
predicate: () => settings.store.alwaysShowUsername
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /,\(0,\i\.jsxs?\)\(\i\.\i,\{.{0,600}#{intl::ACCOUNT_INPUT_OPTIONS}\)\}\)(?=\])/,
|
match: /avatarDecoration:(.{0,70}),size:/,
|
||||||
replace: ""
|
replace: "avatarDecoration:void 0,size:",
|
||||||
|
predicate: () => settings.store.removeAvatarDecoration
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /,\(0,\i\.jsxs?\)\(\i\.\i,\{.{0,650}#{intl::ACCOUNT_OUTPUT_OPTIONS}\)\}\)(?=\])/,
|
match: /displayNameStyles:(\i),/,
|
||||||
replace: ""
|
replace: "displayNameStyles:void 0,",
|
||||||
|
predicate: () => settings.store.removeUsernameStyles
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
predicate: () => settings.store.removeAudioMenus
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
find: '"MicrophoneButton"',
|
||||||
replacement: {
|
|
||||||
match: /hoverText:(\i),forceHover:\i,children:/g,
|
|
||||||
replace: "hoverText:$1,forceHover:!0,children:"
|
|
||||||
},
|
|
||||||
predicate: () => settings.store.alwaysShowUsername
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /:\{tooltipText:\i\};/,
|
match: /:\{tooltipText:\i\};/,
|
||||||
replace: ":{tooltipText:void 0};"
|
replace: ":{tooltipText:void 0};",
|
||||||
|
predicate: () => settings.store.removeButtonTooltips
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
match: /(?<=#{intl::MUTE}\),)className:\i\.\i,/,
|
||||||
|
replace: "",
|
||||||
|
predicate: () => settings.store.removeAudioMenus
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /,\(0,\i\.jsxs?\)\(\i\.\i,\{.{0,600}#{intl::ACCOUNT_INPUT_OPTIONS}\)\}\)(?=\])/,
|
||||||
|
replace: "",
|
||||||
|
predicate: () => settings.store.removeAudioMenus
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "#{intl::f+DDY/::raw},{outputDeviceName",
|
||||||
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=role:"switch",)tooltipText:\i\}/,
|
match: /(?<=role:"switch",)tooltipText:\i\}/,
|
||||||
replace: "tooltipText:void 0}"
|
replace: "tooltipText:void 0}",
|
||||||
|
predicate: () => settings.store.removeButtonTooltips
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /tooltipText:\i,tooltipPositionKey/,
|
match: /(?<=#{intl::DEAFEN}\),)className:\i\.\i,/,
|
||||||
replace: "tooltipText:void 0,tooltipPositionKey"
|
replace: "",
|
||||||
|
predicate: () => settings.store.removeAudioMenus
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /,\(0,\i\.jsxs?\)\(\i\.\i,\{.{0,650}#{intl::ACCOUNT_OUTPUT_OPTIONS}\)\}\)(?=\])/,
|
||||||
|
replace: "",
|
||||||
|
predicate: () => settings.store.removeAudioMenus
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "#{intl::USER_SETTINGS_WITH_BUILD_OVERRIDE}",
|
||||||
|
replacement: {
|
||||||
|
match: /tooltipText:\i,tooltipPositionKey/,
|
||||||
|
replace: "tooltipText:void 0,tooltipPositionKey"
|
||||||
|
},
|
||||||
predicate: () => settings.store.removeButtonTooltips
|
predicate: () => settings.store.removeButtonTooltips
|
||||||
},
|
},
|
||||||
{
|
|
||||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
|
||||||
replacement: {
|
|
||||||
match: /avatarDecoration:(.{0,70}),size:/,
|
|
||||||
replace: "avatarDecoration:void 0,size:"
|
|
||||||
},
|
|
||||||
predicate: () => settings.store.removeAvatarDecoration
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
|
||||||
replacement: {
|
|
||||||
match: /displayNameStyles:(\i),/,
|
|
||||||
replace: "displayNameStyles:void 0,"
|
|
||||||
},
|
|
||||||
predicate: () => settings.store.removeUsernameStyles
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: ']="BADGES"',
|
find: ']="BADGES"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=textDecorationColor:.{0,300})(?=onClick:\i,onContextMenu:\i,)/,
|
match: /(?="data-username-has-gradient")/,
|
||||||
replace: "draggable:!0,onDragStart:e=>$self.onUserDragStart(e,arguments[0].author),\"data-dragify-user\":!0,\"data-user-id\":arguments[0].author.id,"
|
replace: "draggable:!0,onDragStart:e=>$self.onUserDragStart(e,arguments[0].author),\"data-dragify-user\":!0,\"data-user-id\":arguments[0].author.id,"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ function BooIndicator() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{settings.store.showIndicator && getGhostedChannels().length && (
|
{settings.store.showIndicator && getGhostedChannels().length > 0 && (
|
||||||
<div id={cl("container")}>
|
<div id={cl("container")}>
|
||||||
<Tooltip text={getTooltipText()} position="right">
|
<Tooltip text={getTooltipText()} position="right">
|
||||||
{({ onMouseEnter, onMouseLeave }) => (
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@
|
||||||
background-color: var(--background-primary);
|
background-color: var(--background-primary);
|
||||||
height: 30px;
|
height: 30px;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
margin-left: 8px;
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
color: var(--text-default);
|
color: var(--text-default);
|
||||||
fill: var(--text-default);
|
fill: var(--text-default);
|
||||||
}
|
}
|
||||||
|
|
@ -153,4 +154,4 @@
|
||||||
|
|
||||||
.vc-boo-modal-text {
|
.vc-boo-modal-text {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
|
||||||
import { EquicordDevs } from "@utils/index";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
|
|
||||||
const MIDDLE_CLICK = 1;
|
|
||||||
|
|
||||||
let lastMiddleClickUp = 0;
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
scope: {
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
description: "Situations in which to prevent middle click from pasting.",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: "Always Prevent Middle Click Pasting",
|
|
||||||
value: "always",
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Only Prevent When Text Area Not Focused",
|
|
||||||
value: "focus"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
threshold: {
|
|
||||||
type: OptionType.NUMBER,
|
|
||||||
description: "Milliseconds until pasting is enabled again after a middle click.",
|
|
||||||
default: 100,
|
|
||||||
onChange(newValue) { if (newValue < 1) { settings.store.threshold = 1; } },
|
|
||||||
},
|
|
||||||
preventLinkOpen: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Prevent middle-click on links from opening new tabs while preserving autoscroll.",
|
|
||||||
default: false,
|
|
||||||
onChange(newValue) {
|
|
||||||
if (newValue) {
|
|
||||||
document.addEventListener("auxclick", handleAuxClick, true);
|
|
||||||
} else {
|
|
||||||
document.removeEventListener("auxclick", handleAuxClick, true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function shouldBlockLink(e: MouseEvent): boolean {
|
|
||||||
if (e.button !== MIDDLE_CLICK) return false;
|
|
||||||
|
|
||||||
const target = e.target as HTMLElement | null;
|
|
||||||
if (!target?.closest) return false;
|
|
||||||
|
|
||||||
const a = target.closest("a[href]") as HTMLAnchorElement | null;
|
|
||||||
if (!a) return false;
|
|
||||||
|
|
||||||
const href = a.getAttribute("href");
|
|
||||||
return !!href && href !== "#";
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAuxClick(e: MouseEvent) {
|
|
||||||
if (!shouldBlockLink(e)) return;
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "LimitMiddleClickPaste",
|
|
||||||
description: "Prevent middle click pasting either always or just when a text area is not focused.",
|
|
||||||
authors: [EquicordDevs.Etorix, EquicordDevs.korzi],
|
|
||||||
settings,
|
|
||||||
|
|
||||||
isPastingDisabled(isInput: boolean) {
|
|
||||||
const pasteBlocked = Date.now() - lastMiddleClickUp < Math.max(settings.store.threshold, 1);
|
|
||||||
const { scope } = settings.store;
|
|
||||||
|
|
||||||
if (!pasteBlocked) return false;
|
|
||||||
if (scope === "always") return true;
|
|
||||||
if (scope === "focus" && !isInput) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
onMouseUp: (e: MouseEvent) => {
|
|
||||||
if (e.button === MIDDLE_CLICK) lastMiddleClickUp = Date.now();
|
|
||||||
},
|
|
||||||
|
|
||||||
start() {
|
|
||||||
document.addEventListener("mouseup", this.onMouseUp);
|
|
||||||
if (settings.store.preventLinkOpen) {
|
|
||||||
document.addEventListener("auxclick", handleAuxClick, true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
document.removeEventListener("mouseup", this.onMouseUp);
|
|
||||||
document.removeEventListener("auxclick", handleAuxClick, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
// Detects paste events triggered by the "browser" outside of input fields.
|
|
||||||
find: "document.addEventListener(\"paste\",",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=paste",(\i)=>{)/,
|
|
||||||
replace: "if($1.target.tagName===\"BUTTON\"||$self.isPastingDisabled(false)){$1.preventDefault?.();$1.stopPropagation?.();return;};"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Detects paste events triggered inside of Discord's text input.
|
|
||||||
find: ",origin:\"clipboard\"});",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=handlePaste=(\i)=>{)(?=let)/,
|
|
||||||
replace: "if($self.isPastingDisabled(true)){$1.preventDefault?.();$1.stopPropagation?.();return;}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Detects paste events triggered inside of Discord's search box.
|
|
||||||
find: "props.handlePastedText&&",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=clipboardData\);)/,
|
|
||||||
replace: "if($self.isPastingDisabled(true)){arguments[1].preventDefault?.();arguments[1].stopPropagation?.();return;};"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
@ -8,16 +8,17 @@ import "./style.css";
|
||||||
|
|
||||||
import { DecoratorProps } from "@api/MemberListDecorators";
|
import { DecoratorProps } from "@api/MemberListDecorators";
|
||||||
import { isPluginEnabled } from "@api/PluginManager";
|
import { isPluginEnabled } from "@api/PluginManager";
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import betterActivities from "@equicordplugins/betterActivities";
|
import betterActivities from "@equicordplugins/betterActivities";
|
||||||
import showMeYourName from "@plugins/showMeYourName";
|
import showMeYourName from "@plugins/showMeYourName";
|
||||||
import { Devs, EquicordDevs } from "@utils/constants";
|
import { Devs, EquicordDevs } from "@utils/constants";
|
||||||
import { classNameFactory } from "@utils/css";
|
import { classNameFactory } from "@utils/css";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { Activity, ApplicationStream, Channel, Message, OnlineStatus, User } from "@vencord/discord-types";
|
import { Activity, ApplicationStream, Channel, Message, OnlineStatus, User } from "@vencord/discord-types";
|
||||||
import { MessageFlags } from "@vencord/discord-types/enums";
|
import { MessageFlags } from "@vencord/discord-types/enums";
|
||||||
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy, findExportedComponentLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy, findExportedComponentLazy } from "@webpack";
|
||||||
import { ChannelStore, ExperimentStore, MessageStore, Parser, RelationshipStore, SnowflakeUtils, UserStore, useStateFromStores } from "@webpack/common";
|
import { ChannelStore, ExperimentStore, MessageStore, Parser, RelationshipStore, SnowflakeUtils, UserGuildSettingsStore, UserStore, useStateFromStores } from "@webpack/common";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-message-peek-");
|
const cl = classNameFactory("vc-message-peek-");
|
||||||
|
|
||||||
|
|
@ -30,6 +31,14 @@ const ActivityText: React.ComponentType<ActivityTextProps> = findComponentByCode
|
||||||
|
|
||||||
const ONE_HOUR_MS = 60 * 60 * 1000;
|
const ONE_HOUR_MS = 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
hideMuted: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Hide message previews and timestamps for muted DMs and group chats.",
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
type AttachmentType = "image" | "gif" | "video" | "file";
|
type AttachmentType = "image" | "gif" | "video" | "file";
|
||||||
type IconType = AttachmentType | "voice" | "sticker";
|
type IconType = AttachmentType | "voice" | "sticker";
|
||||||
|
|
||||||
|
|
@ -150,7 +159,7 @@ function MessagePreviewContent({ channel, user }: { channel: Channel; user: User
|
||||||
return <>Official Discord Message</>;
|
return <>Official Discord Message</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const smynName = isPluginEnabled(showMeYourName.name) ? showMeYourName.getMemberListProfilesReactionsVoiceNameText({ user: user ?? lastMessage?.author, type: "membersList" }) : null;
|
const smynName = isPluginEnabled(showMeYourName.name) ? showMeYourName.getTypingMemberListProfilesReactionsVoiceNameText({ user: user ?? lastMessage?.author, type: "membersList" }) : null;
|
||||||
|
|
||||||
if (!lastMessage) {
|
if (!lastMessage) {
|
||||||
if (channel.isMultiUserDM()) return <>{channel.recipients.length + 1} Members</>;
|
if (channel.isMultiUserDM()) return <>{channel.recipients.length + 1} Members</>;
|
||||||
|
|
@ -231,6 +240,7 @@ export default definePlugin({
|
||||||
name: "MessagePeek",
|
name: "MessagePeek",
|
||||||
description: "Shows the last message preview and timestamp in the Direct Messages list.",
|
description: "Shows the last message preview and timestamp in the Direct Messages list.",
|
||||||
authors: [Devs.prism, EquicordDevs.justjxke],
|
authors: [Devs.prism, EquicordDevs.justjxke],
|
||||||
|
settings,
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "PrivateChannel.renderAvatar",
|
find: "PrivateChannel.renderAvatar",
|
||||||
|
|
@ -252,11 +262,28 @@ export default definePlugin({
|
||||||
|
|
||||||
renderMemberListDecorator({ channel }: DecoratorProps) {
|
renderMemberListDecorator({ channel }: DecoratorProps) {
|
||||||
if (!channel) return null;
|
if (!channel) return null;
|
||||||
|
if (settings.store.hideMuted && UserGuildSettingsStore.isChannelMuted(null!, channel.id)) return null;
|
||||||
return <Timestamp channel={channel} />;
|
return <Timestamp channel={channel} />;
|
||||||
},
|
},
|
||||||
|
|
||||||
getSubText(props: PrivateChannelProps) {
|
getSubText(props: PrivateChannelProps) {
|
||||||
const { channel, user, activities, status, applicationStream, voiceChannel } = props;
|
const { channel, user, activities, status, applicationStream, voiceChannel } = props;
|
||||||
|
|
||||||
|
if (settings.store.hideMuted && UserGuildSettingsStore.isChannelMuted(null!, channel.id)) {
|
||||||
|
const hasActivity = hasRelevantActivity({ activities, status, applicationStream, voiceChannel });
|
||||||
|
if (hasActivity) {
|
||||||
|
return (
|
||||||
|
<ActivityText
|
||||||
|
user={user}
|
||||||
|
activities={activities}
|
||||||
|
voiceChannel={voiceChannel}
|
||||||
|
applicationStream={applicationStream}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const lastMessage = MessageStore.getLastMessage(channel.id) as Message | undefined;
|
const lastMessage = MessageStore.getLastMessage(channel.id) as Message | undefined;
|
||||||
const hasActivity = hasRelevantActivity({ activities, status, applicationStream, voiceChannel });
|
const hasActivity = hasRelevantActivity({ activities, status, applicationStream, voiceChannel });
|
||||||
const showActivity = shouldShowActivity(lastMessage, hasActivity);
|
const showActivity = shouldShowActivity(lastMessage, hasActivity);
|
||||||
|
|
|
||||||
150
src/equicordplugins/middleClickTweaks/index.ts
Normal file
150
src/equicordplugins/middleClickTweaks/index.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2025 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings, SettingsStore } from "@api/Settings";
|
||||||
|
import { EquicordDevs } from "@utils/index";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
const MIDDLE_CLICK = 1;
|
||||||
|
let lastMiddleClickUp = 0;
|
||||||
|
|
||||||
|
function updateListeners(refresh: boolean = true) {
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp, true);
|
||||||
|
document.removeEventListener("auxclick", handleAuxClick, true);
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
document.addEventListener("mouseup", handleMouseUp, true);
|
||||||
|
document.addEventListener("auxclick", handleAuxClick, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAuxClick(event: MouseEvent) {
|
||||||
|
if (event.button !== MIDDLE_CLICK) return false;
|
||||||
|
|
||||||
|
const { openScope } = settings.store;
|
||||||
|
|
||||||
|
const target = event.target as HTMLElement | null;
|
||||||
|
const anchor = target?.closest("a[href]") as HTMLAnchorElement | null;
|
||||||
|
const media = target?.closest("a[href][data-role='img'], a[href][data-role='video']") as HTMLAnchorElement | null;
|
||||||
|
const role = anchor?.dataset.role ?? "";
|
||||||
|
|
||||||
|
const isMedia = !!media;
|
||||||
|
const isLink = !isMedia && !!anchor?.href && anchor.getAttribute("href") !== "#" && !["img", "video", "button"].includes(role);
|
||||||
|
|
||||||
|
if (isLink && ["links", "both"].includes(openScope)) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else if (isMedia && ["media", "both"].includes(openScope)) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp(event: MouseEvent) {
|
||||||
|
if (event.button === MIDDLE_CLICK) lastMiddleClickUp = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
openScope: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "Prevent middle clicking on these content types from opening them.",
|
||||||
|
options: [
|
||||||
|
{ label: "Links", value: "links" },
|
||||||
|
{ label: "Media", value: "media" },
|
||||||
|
{ label: "Links & Media", value: "both" },
|
||||||
|
{ label: "None", value: "none", default: true },
|
||||||
|
],
|
||||||
|
onChange(newValue) { updateListeners(newValue !== "none"); }
|
||||||
|
},
|
||||||
|
pasteScope: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "Prevent middle click from pasting during these situations.",
|
||||||
|
options: [
|
||||||
|
{ label: "Always Prevent Middle Click Pasting", value: "always", default: true },
|
||||||
|
{ label: "Only Prevent When Text Area Not Focused", value: "focus" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
pasteThreshold: {
|
||||||
|
type: OptionType.NUMBER,
|
||||||
|
description: "Milliseconds until pasting is enabled again after a middle click.",
|
||||||
|
default: 100,
|
||||||
|
onChange(newValue) { if (newValue < 1) { settings.store.pasteThreshold = 1; } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function migrate() {
|
||||||
|
const { plugins } = SettingsStore.plain;
|
||||||
|
const oldPlugin = plugins?.LimitMiddleClickPaste;
|
||||||
|
|
||||||
|
if (!oldPlugin) return;
|
||||||
|
|
||||||
|
const newPlugin = plugins.MiddleClickTweaks ??= { enabled: false };
|
||||||
|
const { scope, threshold, preventLinkOpen } = oldPlugin;
|
||||||
|
|
||||||
|
if (scope) newPlugin.pasteScope = scope;
|
||||||
|
if (threshold) newPlugin.pasteThreshold = threshold;
|
||||||
|
if (preventLinkOpen) newPlugin.openScope = "both";
|
||||||
|
if (oldPlugin.enabled) newPlugin.enabled = true;
|
||||||
|
|
||||||
|
delete plugins.LimitMiddleClickPaste;
|
||||||
|
SettingsStore.markAsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate();
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "MiddleClickTweaks",
|
||||||
|
description: "Various middle click tweaks, such as with pasting and link opening.",
|
||||||
|
authors: [EquicordDevs.Etorix, EquicordDevs.korzi],
|
||||||
|
settings,
|
||||||
|
|
||||||
|
tags: ["LimitMiddleClickPaste"],
|
||||||
|
|
||||||
|
isPastingDisabled(isInput: boolean) {
|
||||||
|
const pasteBlocked = Date.now() - lastMiddleClickUp < Math.max(settings.store.pasteThreshold, 1);
|
||||||
|
const { pasteScope } = settings.store;
|
||||||
|
|
||||||
|
if (!pasteBlocked) return false;
|
||||||
|
if (pasteScope === "always") return true;
|
||||||
|
if (pasteScope === "focus" && !isInput) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
start() {
|
||||||
|
migrate();
|
||||||
|
updateListeners();
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() { updateListeners(false); },
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
// Detects paste events triggered by the "browser" outside of input fields.
|
||||||
|
find: "document.addEventListener(\"paste\",",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=paste",(\i)=>{)/,
|
||||||
|
replace: "if($1.target.tagName===\"BUTTON\"||$self.isPastingDisabled(false)){$1.preventDefault?.();$1.stopPropagation?.();return;};"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Detects paste events triggered inside of Discord's text input.
|
||||||
|
find: ",origin:\"clipboard\"});",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=handlePaste=(\i)=>{)(?=let)/,
|
||||||
|
replace: "if($self.isPastingDisabled(true)){$1.preventDefault?.();$1.stopPropagation?.();return;}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Detects paste events triggered inside of Discord's search box.
|
||||||
|
find: "props.handlePastedText&&",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=clipboardData\);)/,
|
||||||
|
replace: "if($self.isPastingDisabled(true)){arguments[1].preventDefault?.();arguments[1].stopPropagation?.();return;};"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
@ -52,10 +52,10 @@ export default definePlugin({
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||||
replacement: {
|
replacement: {
|
||||||
// react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah })
|
// react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah })
|
||||||
match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,hidePrivateData:)/,
|
match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,(occluded|hidePrivateData):)/,
|
||||||
// react.jsx(WrapperComponent, { VencordOriginal: AccountPanel, ...
|
// react.jsx(WrapperComponent, { VencordOriginal: AccountPanel, ...
|
||||||
replace: "$self.PanelWrapper,{VencordOriginal:$1,"
|
replace: "$self.PanelWrapper,{VencordOriginal:$1,"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ type ProfileCollectible = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type ImageInput = string | { imageUri: string; } | null | undefined;
|
type ImageInput = string | { imageUri: string; } | null | undefined;
|
||||||
|
type DisplayNameStylesLike = DisplayNameStyles & {
|
||||||
|
fontId?: number;
|
||||||
|
effectId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
function dispatch(type: string, payload: Record<string, unknown>) {
|
function dispatch(type: string, payload: Record<string, unknown>) {
|
||||||
FluxDispatcher.dispatch({ type, ...payload });
|
FluxDispatcher.dispatch({ type, ...payload });
|
||||||
|
|
@ -73,6 +77,22 @@ function hasImageInput(value: ImageInput): boolean {
|
||||||
return typeof value === "object" && "imageUri" in value && isNonEmptyString(value.imageUri);
|
return typeof value === "object" && "imageUri" in value && isNonEmptyString(value.imageUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDisplayNameStyles(value: DisplayNameStylesLike | null | undefined): DisplayNameStylesLike | null {
|
||||||
|
if (!value) return null;
|
||||||
|
const fontId = value.fontId ?? value.font_id;
|
||||||
|
const effectId = value.effectId ?? value.effect_id;
|
||||||
|
if (typeof fontId !== "number" || typeof effectId !== "number") return null;
|
||||||
|
const colors = Array.isArray(value.colors) ? [...value.colors] : [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
fontId,
|
||||||
|
effectId,
|
||||||
|
font_id: fontId,
|
||||||
|
effect_id: effectId,
|
||||||
|
colors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function imageUrlToBase64(url: string): Promise<string | null> {
|
export async function imageUrlToBase64(url: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
@ -208,12 +228,11 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil
|
||||||
type: nameplateToUse.type || 2
|
type: nameplateToUse.type || 2
|
||||||
} : null;
|
} : null;
|
||||||
|
|
||||||
const displayNameStylesToUse = pendingChanges.pendingDisplayNameStyles ?? userAny.displayNameStyles;
|
const savedDisplayNameStyles = isGuildProfile
|
||||||
const displayNameStyles = displayNameStylesToUse ? {
|
? (guildMember?.displayNameStyles ?? userAny.displayNameStyles)
|
||||||
font_id: displayNameStylesToUse.font_id ?? 0,
|
: userAny.displayNameStyles;
|
||||||
effect_id: displayNameStylesToUse.effect_id ?? 0,
|
const displayNameStylesToUse = pendingChanges.pendingDisplayNameStyles ?? savedDisplayNameStyles;
|
||||||
colors: Array.isArray(displayNameStylesToUse.colors) ? displayNameStylesToUse.colors : []
|
const displayNameStyles = normalizeDisplayNameStyles(displayNameStylesToUse as DisplayNameStylesLike | null | undefined);
|
||||||
} : null;
|
|
||||||
|
|
||||||
const pendingAvatar = pendingChanges.pendingAvatar ?? pendingChanges.avatar;
|
const pendingAvatar = pendingChanges.pendingAvatar ?? pendingChanges.avatar;
|
||||||
const avatarToUse: ImageInput = hasImageInput(pendingAvatar)
|
const avatarToUse: ImageInput = hasImageInput(pendingAvatar)
|
||||||
|
|
@ -245,7 +264,7 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil
|
||||||
globalName: isGuildProfile
|
globalName: isGuildProfile
|
||||||
? (pendingChanges.pendingNickname ?? guildMember?.nick ?? null)
|
? (pendingChanges.pendingNickname ?? guildMember?.nick ?? null)
|
||||||
: (pendingChanges.pendingGlobalName ?? currentUser.globalName ?? null),
|
: (pendingChanges.pendingGlobalName ?? currentUser.globalName ?? null),
|
||||||
pronouns: pendingChanges.pendingPronouns ?? userProfile?.pronouns ?? null,
|
pronouns: isGuildProfile ? null : (pendingChanges.pendingPronouns ?? userProfile?.pronouns ?? null),
|
||||||
avatarDecoration,
|
avatarDecoration,
|
||||||
profileEffect,
|
profileEffect,
|
||||||
nameplate,
|
nameplate,
|
||||||
|
|
@ -278,11 +297,16 @@ export async function loadPresetAsPending(preset: ProfilePreset, guildId?: strin
|
||||||
const current = await getCurrentProfile(guildId, {
|
const current = await getCurrentProfile(guildId, {
|
||||||
isGuildProfile: isGuild
|
isGuildProfile: isGuild
|
||||||
});
|
});
|
||||||
const profilePrefix = "USER_SETTINGS_ACCOUNT";
|
const profilePrefix = isGuild ? "GUILD_IDENTITY_SETTINGS" : "USER_SETTINGS_ACCOUNT";
|
||||||
const dispatchProfile = (type: string, payload: Record<string, unknown>) => {
|
const dispatchProfile = (type: string, payload: Record<string, unknown>) => {
|
||||||
dispatch(type, isGuild ? { ...payload, guildId } : payload);
|
dispatch(type, isGuild ? { ...payload, guildId } : payload);
|
||||||
};
|
};
|
||||||
const setPending = (type: string, payload: Record<string, unknown>) => dispatchProfile(type, payload);
|
const setPending = (type: string, payload: Record<string, unknown>) => {
|
||||||
|
dispatchProfile(type, payload);
|
||||||
|
if (isGuild && type.startsWith("GUILD_IDENTITY_SETTINGS_")) {
|
||||||
|
dispatchProfile(type.replace("GUILD_IDENTITY_SETTINGS_", "USER_SETTINGS_ACCOUNT_"), payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if ("avatarDataUrl" in preset) {
|
if ("avatarDataUrl" in preset) {
|
||||||
const avatarValue = preset.avatarDataUrl;
|
const avatarValue = preset.avatarDataUrl;
|
||||||
|
|
@ -300,14 +324,14 @@ export async function loadPresetAsPending(preset: ProfilePreset, guildId?: strin
|
||||||
setPending(`${profilePrefix}_SET_PENDING_BIO`, { bio: bioValue });
|
setPending(`${profilePrefix}_SET_PENDING_BIO`, { bio: bioValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.skipPronouns && "pronouns" in preset && preset.pronouns !== current.pronouns) {
|
if (!isGuild && !options.skipPronouns && "pronouns" in preset && preset.pronouns !== current.pronouns) {
|
||||||
const pronounsValue = preset.pronouns === null ? "" : preset.pronouns;
|
const pronounsValue = preset.pronouns === null ? "" : preset.pronouns;
|
||||||
setPending(`${profilePrefix}_SET_PENDING_PRONOUNS`, { pronouns: pronounsValue });
|
setPending(`${profilePrefix}_SET_PENDING_PRONOUNS`, { pronouns: pronounsValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.skipGlobalName && preset.globalName !== undefined && preset.globalName !== current.globalName) {
|
if (!options.skipGlobalName && preset.globalName !== undefined && preset.globalName !== current.globalName) {
|
||||||
if (isGuild) {
|
if (isGuild) {
|
||||||
setPending("USER_SETTINGS_ACCOUNT_SET_PENDING_NICKNAME", { nickname: preset.globalName });
|
setPending("GUILD_IDENTITY_SETTINGS_SET_PENDING_NICKNAME", { nickname: preset.globalName });
|
||||||
} else {
|
} else {
|
||||||
setPending("USER_SETTINGS_ACCOUNT_SET_PENDING_GLOBAL_NAME", { globalName: preset.globalName });
|
setPending("USER_SETTINGS_ACCOUNT_SET_PENDING_GLOBAL_NAME", { globalName: preset.globalName });
|
||||||
}
|
}
|
||||||
|
|
@ -331,8 +355,11 @@ export async function loadPresetAsPending(preset: ProfilePreset, guildId?: strin
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preset.displayNameStyles !== undefined && !jsonEq(preset.displayNameStyles, current.displayNameStyles)) {
|
if (preset.displayNameStyles !== undefined) {
|
||||||
setPending(`${profilePrefix}_SET_PENDING_DISPLAY_NAME_STYLES`, { displayNameStyles: preset.displayNameStyles });
|
const presetDisplayNameStyles = normalizeDisplayNameStyles(preset.displayNameStyles as DisplayNameStylesLike | null | undefined);
|
||||||
|
if (!jsonEq(presetDisplayNameStyles, current.displayNameStyles)) {
|
||||||
|
setPending(`${profilePrefix}_SET_PENDING_DISPLAY_NAME_STYLES`, { displayNameStyles: presetDisplayNameStyles });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preset.themeColors !== undefined && !jsonEq(preset.themeColors, current.themeColors)) {
|
if (preset.themeColors !== undefined && !jsonEq(preset.themeColors, current.themeColors)) {
|
||||||
|
|
@ -360,4 +387,4 @@ export async function loadPresetAsPending(preset: ProfilePreset, guildId?: strin
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1007,6 +1007,8 @@ function getQuestAcceptedButtonText(quest: Quest, prepositional: boolean = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQuestPanelPercentComplete({ quest, percentCompleteText }: { quest: Quest; percentCompleteText?: string; }): { percentComplete: number; } | { percentComplete: number; percentCompleteText: string; } | null {
|
function getQuestPanelPercentComplete({ quest, percentCompleteText }: { quest: Quest; percentCompleteText?: string; }): { percentComplete: number; } | { percentComplete: number; percentCompleteText: string; } | null {
|
||||||
|
if (!quest) { return null; }
|
||||||
|
|
||||||
const task = getQuestTask(quest);
|
const task = getQuestTask(quest);
|
||||||
|
|
||||||
if (!task) { return null; }
|
if (!task) { return null; }
|
||||||
|
|
@ -1294,8 +1296,8 @@ export default definePlugin({
|
||||||
// Fixes the progress tracking for auto-completing Quests.
|
// Fixes the progress tracking for auto-completing Quests.
|
||||||
find: ",{progressTextAnimation:",
|
find: ",{progressTextAnimation:",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=children:\i}=)(\i)/,
|
match: /(let{percentComplete:.{0,115}?children:\i}=)(\i)/,
|
||||||
replace: "Object.assign({},$1,$1.quest?$self.getQuestPanelPercentComplete($1):{})"
|
replace: "const questifyProgress=$self.getQuestPanelPercentComplete({...$2,quest:$2.children?.props?.quest});$1Object.assign({},$2,questifyProgress??{})"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ export function fixUpQuote(quote: string): string {
|
||||||
|
|
||||||
export function generateFileNamePreview(message: string): string {
|
export function generateFileNamePreview(message: string): string {
|
||||||
const words = message.split(" ");
|
const words = message.split(" ");
|
||||||
return words.length > 6 ? words.slice(0, 6).join(" ") : words.join(" ");
|
const preview = words.length > 6 ? words.slice(0, 6).join(" ") : words.join(" ");
|
||||||
|
return preview.slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFileExtension(saveAsGif: boolean): string {
|
export function getFileExtension(saveAsGif: boolean): string {
|
||||||
|
|
@ -153,12 +154,31 @@ function calculateTextLines(
|
||||||
let currentLine: string[] = [];
|
let currentLine: string[] = [];
|
||||||
|
|
||||||
words.forEach(word => {
|
words.forEach(word => {
|
||||||
const testLine = [...currentLine, word].join(" ");
|
if (ctx.measureText(word).width > maxWidth) {
|
||||||
if (ctx.measureText(testLine).width > maxWidth && currentLine.length) {
|
if (currentLine.length) {
|
||||||
lines.push(currentLine.join(" "));
|
lines.push(currentLine.join(" "));
|
||||||
currentLine = [word];
|
currentLine = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk = "";
|
||||||
|
for (const char of word) {
|
||||||
|
const testChunk = chunk + char;
|
||||||
|
if (ctx.measureText(testChunk).width > maxWidth) {
|
||||||
|
if (chunk) lines.push(chunk);
|
||||||
|
chunk = char;
|
||||||
|
} else {
|
||||||
|
chunk = testChunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chunk) lines.push(chunk);
|
||||||
} else {
|
} else {
|
||||||
currentLine.push(word);
|
const testLine = [...currentLine, word].join(" ");
|
||||||
|
if (ctx.measureText(testLine).width > maxWidth && currentLine.length) {
|
||||||
|
lines.push(currentLine.join(" "));
|
||||||
|
currentLine = [word];
|
||||||
|
} else {
|
||||||
|
currentLine.push(word);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,11 @@ const settings = definePluginSettings({
|
||||||
description: "Comma or space separated list of friends' user IDs you want to recieve join messages from",
|
description: "Comma or space separated list of friends' user IDs you want to recieve join messages from",
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
ignoredFriends: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "Comma or space separated list of friends' user IDs you do NOT want to recieve join messages from",
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
ignoreBlockedUsers: {
|
ignoreBlockedUsers: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Do not send messages about blocked users joining/leaving/moving voice channels",
|
description: "Do not send messages about blocked users joining/leaving/moving voice channels",
|
||||||
|
|
@ -89,9 +94,11 @@ function sendVoiceStatusMessage(channelId: string, content: string, userId: stri
|
||||||
|
|
||||||
function isFriendAllowlisted(friendId: string) {
|
function isFriendAllowlisted(friendId: string) {
|
||||||
if (!RelationshipStore.isFriend(friendId)) return false;
|
if (!RelationshipStore.isFriend(friendId)) return false;
|
||||||
const list = settings.store.allowedFriends.split(",").join(" ").split(" ").filter(i => i.length > 0);
|
const ignoreList = settings.store.ignoredFriends.split(",").join(" ").split(" ").filter(i => i.length > 0);
|
||||||
if (list.join(" ").length < 1) return true;
|
if (ignoreList.includes(friendId)) return false;
|
||||||
return list.includes(friendId);
|
const allowList = settings.store.allowedFriends.split(",").join(" ").split(" ").filter(i => i.length > 0);
|
||||||
|
if (allowList.join(" ").length < 1) return true;
|
||||||
|
return allowList.includes(friendId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blatantly stolen from VcNarrator plugin
|
// Blatantly stolen from VcNarrator plugin
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export default definePlugin({
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||||
group: true,
|
group: true,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
|
|
@ -87,7 +87,7 @@ export default definePlugin({
|
||||||
replace: "$&$self.onPopoutClose();"
|
replace: "$&$self.onPopoutClose();"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?:#{intl::5fWB8U::raw}|#{intl::SET_STATUS})\)(?<=ref:(\i),style:.+?)/,
|
match: /ref:(\i),style:\i(?=.{0,250}#{intl::5fWB8U::raw})/,
|
||||||
replace: "$&,onContextMenu:($self.grabRef($1),$self.openAccountPanelContextMenu)"
|
replace: "$&,onContextMenu:($self.grabRef($1),$self.openAccountPanelContextMenu)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ export default definePlugin({
|
||||||
predicate: () => settings.store.disableFade
|
predicate: () => settings.store.disableFade
|
||||||
},
|
},
|
||||||
{ // Load menu TOC eagerly
|
{ // Load menu TOC eagerly
|
||||||
find: "#{intl::USER_SETTINGS_WITH_BUILD_OVERRIDE}",
|
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?=handleOpenSettingsContextMenu=.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)))/,
|
match: /(?=handleOpenSettingsContextMenu=.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)))/,
|
||||||
replace: "_vencordBetterSettingsEagerLoad=(async ()=>$1)();"
|
replace: "_vencordBetterSettingsEagerLoad=(async ()=>$1)();"
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// Current user area, at bottom of channels/dm list
|
// Current user area, at bottom of channels/dm list
|
||||||
{
|
{
|
||||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||||
replacement: [
|
replacement: [
|
||||||
// Use Decor avatar decoration hook
|
// Use Decor avatar decoration hook
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: "useCanFavoriteChannel",
|
find: "useCanFavoriteChannel",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=\i\.id\)\),\i=)\i\.isThread\(\)/,
|
match: /(?<=isFavorite\(\i\.id\).{0,5})\i\.isThread\(\)/,
|
||||||
replace: "false",
|
replace: "false",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -461,10 +461,10 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: 'className:"channelMention",children:',
|
find: '"channel_mention"});',
|
||||||
replacement: {
|
replacement: {
|
||||||
// Show inside voice channel instead of trying to join them when clicking on a channel mention
|
// Show inside voice channel instead of trying to join them when clicking on a channel mention
|
||||||
match: /(?<=getChannel\(\i\);null!=(\i))(?=.{0,100}?selectVoiceChannel)/,
|
match: /(?<=getChannel\(\i\);if\(null!=(\i))(?=.{0,150}?selectVoiceChannel)/,
|
||||||
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`
|
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { definePluginSettings } from "@api/Settings";
|
||||||
import { Button, TextButton } from "@components/Button";
|
import { Button, TextButton } from "@components/Button";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Heading } from "@components/Heading";
|
import { Heading } from "@components/Heading";
|
||||||
|
import ircColors from "@plugins/ircColors";
|
||||||
import mentionAvatars from "@plugins/mentionAvatars";
|
import mentionAvatars from "@plugins/mentionAvatars";
|
||||||
import { Devs, EquicordDevs } from "@utils/constants";
|
import { Devs, EquicordDevs } from "@utils/constants";
|
||||||
import { classNameFactory, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/index";
|
import { classNameFactory, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/index";
|
||||||
|
|
@ -28,7 +29,6 @@ const wrapEmojis = findByCodeLazy("lastIndex;return");
|
||||||
const adjustColor = findByCodeLazy("light1", "dark1", "toonStroke");
|
const adjustColor = findByCodeLazy("light1", "dark1", "toonStroke");
|
||||||
const AccessibilityStore = findStoreLazy("AccessibilityStore");
|
const AccessibilityStore = findStoreLazy("AccessibilityStore");
|
||||||
|
|
||||||
const colorPattern = /^#(?:[\da-f]{3}){1,2}$|^#(?:[\da-f]{4}){1,2}$|(rgb|hsl)a?\((\s*-?\d+%?\s*,){2}(\s*-?\d+%?\s*)\)|(rgb|hsl)a?\((\s*-?\d+%?\s*,){3}\s*(0|(0?\.\d+)|1)\)$/iu;
|
|
||||||
const roleColorPattern = /^role((?:\+|-)\d{0,4})?$/iu;
|
const roleColorPattern = /^role((?:\+|-)\d{0,4})?$/iu;
|
||||||
const symbolPattern = /^[\p{S}\p{P}]{1,3}$/iu;
|
const symbolPattern = /^[\p{S}\p{P}]{1,3}$/iu;
|
||||||
const templatePattern = /(?:\{(?:custom|friend|nick|display|user)(?:,\s*(?:custom|friend|nick|display|user))*\})/iu;
|
const templatePattern = /(?:\{(?:custom|friend|nick|display|user)(?:,\s*(?:custom|friend|nick|display|user))*\})/iu;
|
||||||
|
|
@ -36,29 +36,59 @@ const templatePattern = /(?:\{(?:custom|friend|nick|display|user)(?:,\s*(?:custo
|
||||||
type CustomNicknameData = Record<string, string>;
|
type CustomNicknameData = Record<string, string>;
|
||||||
let customNicknames: CustomNicknameData = {};
|
let customNicknames: CustomNicknameData = {};
|
||||||
|
|
||||||
function adjustHex(color: string, percent: number): string {
|
let toCSSCache: Map<string, string | null> | null = null;
|
||||||
let hex = color.replace("#", "");
|
let toCSSProbe: HTMLDivElement | null = null;
|
||||||
|
|
||||||
if (hex.length === 3) {
|
function toCSS(color: string | number | null | undefined): string | null {
|
||||||
hex = hex.split("").map(c => c + c).join("");
|
if (color == null) return null;
|
||||||
}
|
if (typeof color === "number") return `#${color.toString(16).padStart(6, "0")}`;
|
||||||
|
if (!color) return null;
|
||||||
|
|
||||||
const num = parseInt(hex, 16);
|
const cached = toCSSCache?.get(color);
|
||||||
let r = (num >> 16) & 0xFF;
|
if (cached !== undefined) return cached ?? null;
|
||||||
let g = (num >> 8) & 0xFF;
|
|
||||||
let b = num & 0xFF;
|
|
||||||
|
|
||||||
|
if (!toCSSProbe) return null;
|
||||||
|
|
||||||
|
toCSSProbe.style.color = "";
|
||||||
|
toCSSProbe.style.color = color;
|
||||||
|
const result = toCSSProbe.style.color !== "" ? color : null;
|
||||||
|
toCSSCache?.set(color, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let convertToRGBCanvas: HTMLCanvasElement | null = null;
|
||||||
|
let convertToRGBCtx: CanvasRenderingContext2D | null = null;
|
||||||
|
let convertToRGBCache: Map<string, [number, number, number] | null> | null = null;
|
||||||
|
|
||||||
|
function convertToRGB(color: string): [number, number, number] | null {
|
||||||
|
const cached = convertToRGBCache?.get(color);
|
||||||
|
if (cached !== undefined) return cached ?? null;
|
||||||
|
|
||||||
|
if (!convertToRGBCanvas || !convertToRGBCtx) return null;
|
||||||
|
|
||||||
|
convertToRGBCtx.fillStyle = "#000000";
|
||||||
|
convertToRGBCtx.clearRect(0, 0, 1, 1);
|
||||||
|
convertToRGBCtx.fillStyle = color;
|
||||||
|
convertToRGBCtx.fillRect(0, 0, 1, 1);
|
||||||
|
const [r, g, b] = convertToRGBCtx.getImageData(0, 0, 1, 1).data;
|
||||||
|
const result: [number, number, number] = [r, g, b];
|
||||||
|
convertToRGBCache?.set(color, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustBrightness(color: string, percent: number): string {
|
||||||
|
const rgb = convertToRGB(color);
|
||||||
|
if (!rgb) return color;
|
||||||
|
|
||||||
|
let [r, g, b] = rgb;
|
||||||
r = Math.max(0, Math.min(255, r + Math.round(r * (percent / 100))));
|
r = Math.max(0, Math.min(255, r + Math.round(r * (percent / 100))));
|
||||||
g = Math.max(0, Math.min(255, g + Math.round(g * (percent / 100))));
|
g = Math.max(0, Math.min(255, g + Math.round(g * (percent / 100))));
|
||||||
b = Math.max(0, Math.min(255, b + Math.round(b * (percent / 100))));
|
b = Math.max(0, Math.min(255, b + Math.round(b * (percent / 100))));
|
||||||
|
|
||||||
const newColor = ((r << 16) | (g << 8) | b).toString(16).padStart(6, "0");
|
const hex = `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}`;
|
||||||
|
if (hex === "#ffffff" || hex === "#000000") return color;
|
||||||
|
|
||||||
if (newColor === "ffffff" || newColor === "000000") {
|
return hex;
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `#${newColor}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validColor(color: string) {
|
function validColor(color: string) {
|
||||||
|
|
@ -73,66 +103,43 @@ function validColor(color: string) {
|
||||||
return !isNaN(percentage) && percentage <= 100 && percentage >= -100;
|
return !isNaN(percentage) && percentage <= 100 && percentage >= -100;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorTestDiv = document.createElement("div");
|
return !!toCSS(color);
|
||||||
colorTestDiv.style.borderColor = color;
|
|
||||||
|
|
||||||
const isValid = colorTestDiv.style.borderColor !== "" && colorPattern.test(color);
|
|
||||||
colorTestDiv.remove();
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveColor(
|
function resolveColor(
|
||||||
user: User | GuildMember,
|
colorStrings: colorStringsType,
|
||||||
|
displayNameStyles: { effectId: number; colors: number[]; } | null | undefined,
|
||||||
savedColor: string,
|
savedColor: string,
|
||||||
canUseGradient: boolean,
|
canUseGradient: boolean,
|
||||||
inGuild: boolean,
|
inGuild: boolean,
|
||||||
forceWhite: boolean
|
forceDefault: boolean
|
||||||
): Record<string, any> | null {
|
): Record<string, any> | null {
|
||||||
if (!savedColor.trim()) { return null; }
|
if (!savedColor.trim()) { return null; }
|
||||||
|
|
||||||
let gradient: any = null;
|
let gradient: any = null;
|
||||||
let primaryColor: any = savedColor;
|
let primaryColor: any = null;
|
||||||
let secondaryColor: any = savedColor;
|
let secondaryColor: any = null;
|
||||||
let tertiaryColor: any = savedColor;
|
let tertiaryColor: any = null;
|
||||||
let primaryAdjusted: any = savedColor;
|
let primaryAdjusted: any = null;
|
||||||
let secondaryAdjusted: any = savedColor;
|
let secondaryAdjusted: any = null;
|
||||||
let tertiaryAdjusted: any = savedColor;
|
let tertiaryAdjusted: any = null;
|
||||||
|
|
||||||
if (savedColor.toLowerCase().includes("role")) {
|
if (savedColor.toLowerCase().includes("role")) {
|
||||||
const percentage = roleColorPattern.exec(savedColor)?.[1] || "";
|
const percentage = roleColorPattern.exec(savedColor)?.[1] || "";
|
||||||
if (percentage && isNaN(parseInt(percentage))) return null;
|
if (percentage && isNaN(parseInt(percentage))) return null;
|
||||||
|
|
||||||
const colorStrings = (user as any)?.colorStrings || {};
|
primaryColor = forceDefault ? toCSS("var(--text-strong)") : (toCSS(colorStrings?.primaryColor) || (!inGuild && toCSS(displayNameStyles?.colors?.[0])) || toCSS("var(--text-strong)"));
|
||||||
const displayStyles = !inGuild ? (user as any)?.displayNameStyles || {} : {};
|
secondaryColor = forceDefault ? null : (toCSS(colorStrings?.secondaryColor) || (!inGuild && toCSS(displayNameStyles?.colors?.[1])) || null);
|
||||||
|
tertiaryColor = forceDefault ? null : (toCSS(colorStrings?.tertiaryColor) || (!inGuild && toCSS(displayNameStyles?.colors?.[2])) || null);
|
||||||
|
|
||||||
primaryColor = forceWhite ? "#ffffff" : colorStrings.primaryColor || displayStyles?.colors?.[0] || "#ffffff";
|
primaryAdjusted = percentage ? adjustBrightness(primaryColor, parseInt(percentage)) : primaryColor;
|
||||||
if (typeof primaryColor === "number") primaryColor = `#${primaryColor.toString(16).padStart(6, "0")}`;
|
secondaryAdjusted = secondaryColor && percentage ? adjustBrightness(secondaryColor, parseInt(percentage)) : secondaryColor;
|
||||||
|
tertiaryAdjusted = tertiaryColor && percentage ? adjustBrightness(tertiaryColor, parseInt(percentage)) : tertiaryColor;
|
||||||
secondaryColor = forceWhite ? null : colorStrings.secondaryColor || displayStyles?.colors?.[1] || null;
|
} else {
|
||||||
if (typeof secondaryColor === "number") secondaryColor = `#${secondaryColor.toString(16).padStart(6, "0")}`;
|
primaryColor = savedColor;
|
||||||
|
|
||||||
tertiaryColor = forceWhite ? null : colorStrings.tertiaryColor || displayStyles?.colors?.[2] || null;
|
|
||||||
if (typeof tertiaryColor === "number") tertiaryColor = `#${tertiaryColor.toString(16).padStart(6, "0")}`;
|
|
||||||
|
|
||||||
primaryAdjusted = primaryColor;
|
|
||||||
secondaryAdjusted = secondaryColor;
|
|
||||||
tertiaryAdjusted = tertiaryColor;
|
|
||||||
|
|
||||||
if (primaryColor && percentage) {
|
|
||||||
primaryAdjusted = adjustHex(primaryColor, parseInt(percentage));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secondaryColor && percentage) {
|
|
||||||
secondaryAdjusted = adjustHex(secondaryColor, parseInt(percentage));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tertiaryColor && percentage) {
|
|
||||||
tertiaryAdjusted = adjustHex(tertiaryColor, parseInt(percentage));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gradient = !canUseGradient || !secondaryColor || forceWhite
|
gradient = !canUseGradient || !secondaryColor || forceDefault
|
||||||
? null
|
? null
|
||||||
: tertiaryColor
|
: tertiaryColor
|
||||||
? "linear-gradient(to right,var(--custom-gradient-color-1),var(--custom-gradient-color-2),var(--custom-gradient-color-3),var(--custom-gradient-color-1))"
|
? "linear-gradient(to right,var(--custom-gradient-color-1),var(--custom-gradient-color-2),var(--custom-gradient-color-3),var(--custom-gradient-color-1))"
|
||||||
|
|
@ -164,7 +171,7 @@ function resolveColor(
|
||||||
"--custom-gradient-color-2": secondaryColor || primaryColor,
|
"--custom-gradient-color-2": secondaryColor || primaryColor,
|
||||||
"--custom-gradient-color-3": tertiaryColor || primaryColor,
|
"--custom-gradient-color-3": tertiaryColor || primaryColor,
|
||||||
"background-image": gradient,
|
"background-image": gradient,
|
||||||
"animation": "smyn-animation 1.5s linear infinite"
|
"animation": "smyn-animation var(--smyn-gradient-duration) linear infinite"
|
||||||
},
|
},
|
||||||
static: {
|
static: {
|
||||||
original: {
|
original: {
|
||||||
|
|
@ -300,6 +307,8 @@ interface mentionProps {
|
||||||
|
|
||||||
interface messageProps {
|
interface messageProps {
|
||||||
message: Message | null | undefined;
|
message: Message | null | undefined;
|
||||||
|
colorString?: string;
|
||||||
|
colorStrings: colorStringsType;
|
||||||
userOverride?: User;
|
userOverride?: User;
|
||||||
isRepliedMessage?: boolean;
|
isRepliedMessage?: boolean;
|
||||||
withMentionPrefix?: boolean;
|
withMentionPrefix?: boolean;
|
||||||
|
|
@ -307,12 +316,14 @@ interface messageProps {
|
||||||
|
|
||||||
interface memberListProfileReactionProps {
|
interface memberListProfileReactionProps {
|
||||||
user: User | null | undefined;
|
user: User | null | undefined;
|
||||||
type: "membersList" | "profilesPopout" | "profilesTooltip" | "reactionsTooltip" | "reactionsPopout" | "voiceChannel";
|
type: "typingIndicator" | "membersList" | "profilesPopout" | "profilesTooltip" | "reactionsTooltip" | "reactionsPopout" | "voiceChannel";
|
||||||
guildId?: string;
|
guildId?: string;
|
||||||
tags?: any;
|
tags?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMemberListProfilesReactionsVoiceName(
|
type colorStringsType = { primaryColor: string | null, secondaryColor: string | null, tertiaryColor: string | null; } | null | undefined;
|
||||||
|
|
||||||
|
function getTypingMemberListProfilesReactionsVoiceName(
|
||||||
props: memberListProfileReactionProps,
|
props: memberListProfileReactionProps,
|
||||||
): [string | null, JSX.Element | null, string | null] {
|
): [string | null, JSX.Element | null, string | null] {
|
||||||
const { user, type } = props;
|
const { user, type } = props;
|
||||||
|
|
@ -321,16 +332,16 @@ function getMemberListProfilesReactionsVoiceName(
|
||||||
const guildId = props.guildId || props.tags?.props?.displayProfile?.guildId || null;
|
const guildId = props.guildId || props.tags?.props?.displayProfile?.guildId || null;
|
||||||
const member = guildId && user ? GuildMemberStore.getMember(guildId, user.id) : null;
|
const member = guildId && user ? GuildMemberStore.getMember(guildId, user.id) : null;
|
||||||
const author = user && member ? { ...user, ...member } : user || member || null;
|
const author = user && member ? { ...user, ...member } : user || member || null;
|
||||||
const shouldHookless = type === "reactionsTooltip" || type === "profilesTooltip";
|
const shouldHookless = ["typingIndicator", "reactionsTooltip", "profilesTooltip"].includes(type);
|
||||||
return renderUsername(author, null, null, type, "", shouldHookless, !!guildId);
|
return renderUsername(author, null, null, type, "", shouldHookless, !!guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMemberListProfilesReactionsVoiceNameText(props: memberListProfileReactionProps): string | null {
|
function getTypingMemberListProfilesReactionsVoiceNameText(props: memberListProfileReactionProps): string | null {
|
||||||
return getMemberListProfilesReactionsVoiceName(props)[2];
|
return getTypingMemberListProfilesReactionsVoiceName(props)[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMemberListProfilesReactionsVoiceNameElement(props: memberListProfileReactionProps): JSX.Element | null {
|
function getTypingMemberListProfilesReactionsVoiceNameElement(props: memberListProfileReactionProps): JSX.Element | null {
|
||||||
return getMemberListProfilesReactionsVoiceName(props)[1];
|
return getTypingMemberListProfilesReactionsVoiceName(props)[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessageName(props: messageProps): [string | null, JSX.Element | null, string | null] {
|
function getMessageName(props: messageProps): [string | null, JSX.Element | null, string | null] {
|
||||||
|
|
@ -343,7 +354,7 @@ function getMessageName(props: messageProps): [string | null, JSX.Element | null
|
||||||
const member = isWebhook ? null : target && channel ? GuildMemberStore.getMember(channel.guild_id, target.id) : null;
|
const member = isWebhook ? null : target && channel ? GuildMemberStore.getMember(channel.guild_id, target.id) : null;
|
||||||
const author = user && member ? { ...user, ...member } : user || member || null;
|
const author = user && member ? { ...user, ...member } : user || member || null;
|
||||||
const mentionSymbol = hideDefaultAtSign && (!isRepliedMessage || replies) ? "" : withMentionPrefix ? "@" : "";
|
const mentionSymbol = hideDefaultAtSign && (!isRepliedMessage || replies) ? "" : withMentionPrefix ? "@" : "";
|
||||||
return renderUsername(author, channel?.id || null, message?.id || null, isRepliedMessage ? "replies" : "messages", mentionSymbol, false, !!channel?.guild_id);
|
return renderUsername(author, channel?.id || null, message?.id || null, isRepliedMessage ? "replies" : "messages", mentionSymbol, false, !!channel?.guild_id, props.colorString, props.colorStrings);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessageNameElement(props: messageProps): JSX.Element | null {
|
function getMessageNameElement(props: messageProps): JSX.Element | null {
|
||||||
|
|
@ -362,7 +373,20 @@ function getMentionNameElement(props: mentionProps): JSX.Element | null {
|
||||||
const member = channel ? GuildMemberStore.getMember(channel.guild_id, userId) : null;
|
const member = channel ? GuildMemberStore.getMember(channel.guild_id, userId) : null;
|
||||||
const author = user && member ? { ...user, ...member } : user || member || null;
|
const author = user && member ? { ...user, ...member } : user || member || null;
|
||||||
const mentionSymbol = hideDefaultAtSign && mentions ? "" : "@";
|
const mentionSymbol = hideDefaultAtSign && mentions ? "" : "@";
|
||||||
return renderUsername(author, channelId || null, nestedProps?.messageId || null, "mentions", mentionSymbol, false, !!channel?.guild_id)[1];
|
|
||||||
|
let colorString: string | undefined = undefined;
|
||||||
|
let colorStrings: colorStringsType = undefined;
|
||||||
|
|
||||||
|
if (isPluginEnabled(ircColors.name)) {
|
||||||
|
const color = ircColors.calculateNameColorForMessageContext({ message: { author: { id: userId } }, channel });
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
colorString = color;
|
||||||
|
colorStrings = { primaryColor: color, secondaryColor: null, tertiaryColor: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderUsername(author, channelId || null, nestedProps?.messageId || null, "mentions", mentionSymbol, false, !!channel?.guild_id, colorString, colorStrings)[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectType(effectId: number | null | undefined): string | null {
|
function getEffectType(effectId: number | null | undefined): string | null {
|
||||||
|
|
@ -405,14 +429,17 @@ function renderUsername(
|
||||||
author: User | GuildMember | null,
|
author: User | GuildMember | null,
|
||||||
channelId: string | null,
|
channelId: string | null,
|
||||||
messageId: string | null,
|
messageId: string | null,
|
||||||
type: "messages" | "replies" | "mentions" | "membersList" | "profilesPopout" | "profilesTooltip" | "reactionsTooltip" | "reactionsPopout" | "voiceChannel",
|
type: "messages" | "replies" | "typingIndicator" | "mentions" | "membersList" | "profilesPopout" | "profilesTooltip" | "reactionsTooltip" | "reactionsPopout" | "voiceChannel",
|
||||||
mentionSymbol: string,
|
mentionSymbol: string,
|
||||||
hookless: boolean,
|
hookless: boolean,
|
||||||
inGuild: boolean
|
inGuild: boolean,
|
||||||
|
colorString?: string,
|
||||||
|
colorStrings?: { primaryColor: string | null, secondaryColor: string | null, tertiaryColor: string | null; } | null
|
||||||
): [string | null, JSX.Element | null, string | null] {
|
): [string | null, JSX.Element | null, string | null] {
|
||||||
const isMessage = type === "messages";
|
const isMessage = type === "messages";
|
||||||
const isReply = type === "replies";
|
const isReply = type === "replies";
|
||||||
const isMention = type === "mentions";
|
const isMention = type === "mentions";
|
||||||
|
const isTyping = type === "typingIndicator";
|
||||||
const isMember = type === "membersList";
|
const isMember = type === "membersList";
|
||||||
const isProfile = type === "profilesPopout";
|
const isProfile = type === "profilesPopout";
|
||||||
const isReactionsPopout = type === "reactionsPopout";
|
const isReactionsPopout = type === "reactionsPopout";
|
||||||
|
|
@ -420,8 +447,8 @@ function renderUsername(
|
||||||
const isReaction = isReactionsTooltip || isReactionsPopout;
|
const isReaction = isReactionsTooltip || isReactionsPopout;
|
||||||
const isVoice = type === "voiceChannel";
|
const isVoice = type === "voiceChannel";
|
||||||
|
|
||||||
const config = hookless ? settings.store : settings.use(["messages", "replies", "mentions", "memberList", "profilePopout", "reactions", "friendNameOnlyInDirectMessages", "customNameOnlyInDirectMessages", "discriminators", "hideDefaultAtSign", "truncateAllNamesWithStreamerMode", "removeDuplicates", "ignoreGradients", "ignoreFonts", "animateGradients", "includedNames", "customNameColor", "friendNameColor", "nicknameColor", "displayNameColor", "usernameColor", "nameSeparator", "triggerNameRerender"]);
|
const config = hookless ? settings.store : settings.use(["messages", "replies", "mentions", "typingIndicator", "memberList", "profilePopout", "reactions", "friendNameOnlyInDirectMessages", "customNameOnlyInDirectMessages", "discriminators", "hideDefaultAtSign", "truncateAllNamesWithStreamerMode", "removeDuplicates", "ignoreGradients", "ignoreFonts", "animateGradients", "includedNames", "customNameColor", "friendNameColor", "nicknameColor", "displayNameColor", "usernameColor", "nameSeparator", "triggerNameRerender"]);
|
||||||
const { messages, replies, mentions, memberList, profilePopout, reactions, friendNameOnlyInDirectMessages, customNameOnlyInDirectMessages, discriminators, truncateAllNamesWithStreamerMode, removeDuplicates, ignoreGradients, ignoreFonts, animateGradients, includedNames, customNameColor, friendNameColor, nicknameColor, displayNameColor, usernameColor, nameSeparator, triggerNameRerender } = config;
|
const { messages, replies, mentions, typingIndicator, memberList, profilePopout, reactions, friendNameOnlyInDirectMessages, customNameOnlyInDirectMessages, discriminators, truncateAllNamesWithStreamerMode, removeDuplicates, ignoreGradients, ignoreFonts, animateGradients, includedNames, customNameColor, friendNameColor, nicknameColor, displayNameColor, usernameColor, nameSeparator, triggerNameRerender } = config;
|
||||||
|
|
||||||
const channel = channelId ? ChannelStore.getChannel(channelId) || null : null;
|
const channel = channelId ? ChannelStore.getChannel(channelId) || null : null;
|
||||||
const message = channelId && messageId ? MessageStore.getMessage(channelId, messageId) : null;
|
const message = channelId && messageId ? MessageStore.getMessage(channelId, messageId) : null;
|
||||||
|
|
@ -435,27 +462,38 @@ function renderUsername(
|
||||||
? hoveringReactionPopoutSet.has((author as User).id)
|
? hoveringReactionPopoutSet.has((author as User).id)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
const authorDisplayNameStyles = (!inGuild && (author as any)?.displayNameStyles) || null;
|
if (colorString && !colorStrings) {
|
||||||
|
colorStrings = {
|
||||||
|
primaryColor: colorString,
|
||||||
|
secondaryColor: null,
|
||||||
|
tertiaryColor: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ircColorsEnabled = isPluginEnabled(ircColors.name);
|
||||||
|
|
||||||
|
const authorColorStrings = colorStrings || (author as any)?.colorStrings || null;
|
||||||
|
const authorDisplayNameStyles = (!inGuild && !ircColorsEnabled && (author as any)?.displayNameStyles) || null;
|
||||||
const effectType = authorDisplayNameStyles ? getEffectType(authorDisplayNameStyles.effectId) : null;
|
const effectType = authorDisplayNameStyles ? getEffectType(authorDisplayNameStyles.effectId) : null;
|
||||||
const effectCSSVars = authorDisplayNameStyles ? computeEffectCSSVars(authorDisplayNameStyles) : {};
|
const effectCSSVars = authorDisplayNameStyles ? computeEffectCSSVars(authorDisplayNameStyles) : {};
|
||||||
const hasEffect = !!effectType;
|
const hasEffect = !!effectType;
|
||||||
const needsEffectDataAttr = effectType === "neon" || effectType === "toon" || effectType === "pop";
|
const needsEffectDataAttr = effectType === "neon" || effectType === "toon" || effectType === "pop";
|
||||||
const shouldShowEffect = hasEffect && isHovering;
|
const shouldShowEffect = hasEffect && isHovering;
|
||||||
const shouldAnimateEffect = shouldShowEffect && !AccessibilityStore.useReducedMotion;
|
const shouldAnimateEffect = shouldShowEffect && !AccessibilityStore.useReducedMotion;
|
||||||
const shouldUseDMDefault = !inGuild && !isHovering;
|
const shouldUseDMDefault = !inGuild && !ircColorsEnabled && !isHovering;
|
||||||
|
|
||||||
const canUseGradient = ((author as GuildMember)?.guildId ? (GuildStore.getGuild((author as GuildMember).guildId) ?? {}).premiumFeatures?.features.includes("ENHANCED_ROLE_COLORS") : !inGuild);
|
const canUseGradient = ((author as GuildMember)?.guildId ? (GuildStore.getGuild((author as GuildMember).guildId) ?? {}).premiumFeatures?.features.includes("ENHANCED_ROLE_COLORS") : !inGuild);
|
||||||
const useTopRoleStyle = isMention || isReactionsPopout || channel?.isDM() || channel?.isGroupDM();
|
const useTopRoleStyle = isMention || isReactionsPopout || channel?.isDM() || channel?.isGroupDM();
|
||||||
const topRoleStyle = author ? resolveColor(author, "Role", canUseGradient, inGuild, shouldUseDMDefault) : null;
|
const topRoleStyle = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, "Role", canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||||
const hasGradient = !!topRoleStyle?.gradient && Object.keys(topRoleStyle.gradient).length > 0;
|
const hasGradient = !!topRoleStyle?.gradient && Object.keys(topRoleStyle.gradient).length > 0;
|
||||||
|
|
||||||
const textMutedValue = getComputedStyle(document.documentElement)?.getPropertyValue("--text-muted")?.trim() || "#72767d";
|
const textMutedValue = getComputedStyle(document.documentElement)?.getPropertyValue("--text-muted")?.trim() || "#72767d";
|
||||||
const options = splitTemplate(includedNames);
|
const options = splitTemplate(includedNames);
|
||||||
const resolvedUsernameColor = author ? resolveColor(author, usernameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
const resolvedUsernameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, usernameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||||
const resolvedDisplayNameColor = author ? resolveColor(author, displayNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
const resolvedDisplayNameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, displayNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||||
const resolvedNicknameColor = author ? resolveColor(author, nicknameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
const resolvedNicknameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, nicknameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||||
const resolvedFriendNameColor = author ? resolveColor(author, friendNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
const resolvedFriendNameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, friendNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||||
const resolvedCustomNameColor = author ? resolveColor(author, customNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
const resolvedCustomNameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, customNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||||
const affixColor = { color: textMutedValue, "-webkit-text-fill-color": textMutedValue, isolation: "isolate", "white-space": "pre", "font-family": "var(--font-primary)", "letter-spacing": "normal" };
|
const affixColor = { color: textMutedValue, "-webkit-text-fill-color": textMutedValue, isolation: "isolate", "white-space": "pre", "font-family": "var(--font-primary)", "letter-spacing": "normal" };
|
||||||
const { username, display, nick, friend, custom } = getProcessedNames(author, truncateAllNamesWithStreamerMode, discriminators, inGuild, friendNameOnlyInDirectMessages, customNameOnlyInDirectMessages);
|
const { username, display, nick, friend, custom } = getProcessedNames(author, truncateAllNamesWithStreamerMode, discriminators, inGuild, friendNameOnlyInDirectMessages, customNameOnlyInDirectMessages);
|
||||||
|
|
||||||
|
|
@ -551,6 +589,8 @@ function renderUsername(
|
||||||
return [null, null, null];
|
return [null, null, null];
|
||||||
} else if (isMention && !mentions) {
|
} else if (isMention && !mentions) {
|
||||||
return [null, null, null];
|
return [null, null, null];
|
||||||
|
} else if (isTyping && !typingIndicator) {
|
||||||
|
return [null, null, null];
|
||||||
} else if (isMember && !memberList) {
|
} else if (isMember && !memberList) {
|
||||||
return [null, null, null];
|
return [null, null, null];
|
||||||
} else if (isProfile && !profilePopout) {
|
} else if (isProfile && !profilePopout) {
|
||||||
|
|
@ -580,6 +620,7 @@ function renderUsername(
|
||||||
}
|
}
|
||||||
|
|
||||||
const remainingNames = [first, second, third, fourth, fifth].filter(Boolean);
|
const remainingNames = [first, second, third, fourth, fifth].filter(Boolean);
|
||||||
|
|
||||||
first = remainingNames.shift();
|
first = remainingNames.shift();
|
||||||
second = remainingNames.shift();
|
second = remainingNames.shift();
|
||||||
third = remainingNames.shift();
|
third = remainingNames.shift();
|
||||||
|
|
@ -591,37 +632,40 @@ function renderUsername(
|
||||||
const shouldAnimateSecondaryNames = animateGradients && !ignoreGradients;
|
const shouldAnimateSecondaryNames = animateGradients && !ignoreGradients;
|
||||||
|
|
||||||
const firstDataText = mentionSymbol + first.name;
|
const firstDataText = mentionSymbol + first.name;
|
||||||
const secondDataText = second && shouldAnimateSecondaryNames ? (second.prefix + second.name + second.suffix) : "";
|
const secondDataText = second && shouldAnimateSecondaryNames ? second.name : "";
|
||||||
const thirdDataText = third && shouldAnimateSecondaryNames ? (third.prefix + third.name + third.suffix) : "";
|
const thirdDataText = third && shouldAnimateSecondaryNames ? third.name : "";
|
||||||
const fourthDataText = fourth && shouldAnimateSecondaryNames ? (fourth.prefix + fourth.name + fourth.suffix) : "";
|
const fourthDataText = fourth && shouldAnimateSecondaryNames ? fourth.name : "";
|
||||||
const fifthDataText = fifth && shouldAnimateSecondaryNames ? (fifth.prefix + fifth.name + fifth.suffix) : "";
|
const fifthDataText = fifth && shouldAnimateSecondaryNames ? fifth.name : "";
|
||||||
const allDataText = [firstDataText, secondDataText, thirdDataText, fourthDataText, fifthDataText].filter(Boolean).join(nameSeparator).trim();
|
const allDataText = [firstDataText, secondDataText, thirdDataText, fourthDataText, fifthDataText].filter(Boolean).join(nameSeparator).trim();
|
||||||
|
|
||||||
// Only mentions and reactions popouts should patch in the gradient glow or else a double glow will appear on messages.
|
// Only mentions and reactions popouts should patch in the gradient glow or else a double glow will appear on messages.
|
||||||
const hoveringClass = (isHovering ? " show-me-your-name-gradient-hovered" : "");
|
const hoveringClass = (isHovering ? " smyn-gradient-hovered" : "");
|
||||||
const gradientClasses = useTopRoleStyle
|
const gradientClasses = useTopRoleStyle
|
||||||
? "show-me-your-name-gradient show-me-your-name-gradient-inherit-bg" + hoveringClass
|
? "smyn-gradient smyn-gradient-inherit-bg" + hoveringClass
|
||||||
: "show-me-your-name-gradient show-me-your-name-gradient-unset-bg" + hoveringClass;
|
: "smyn-gradient smyn-gradient-unset-bg" + hoveringClass;
|
||||||
|
|
||||||
const firstGroupClasses = "show-me-your-name-name-group show-me-your-name-first-name-group";
|
const firstGroupClasses = "smyn-name-group smyn-first-name-group";
|
||||||
const secondGroupClasses = "show-me-your-name-name-group show-me-your-name-second-name-group";
|
const secondGroupClasses = "smyn-name-group smyn-second-name-group";
|
||||||
const thirdGroupClasses = "show-me-your-name-name-group show-me-your-name-third-name-group";
|
const thirdGroupClasses = "smyn-name-group smyn-third-name-group";
|
||||||
const fourthGroupClasses = "show-me-your-name-name-group show-me-your-name-fourth-name-group";
|
const fourthGroupClasses = "smyn-name-group smyn-fourth-name-group";
|
||||||
const fifthGroupClasses = "show-me-your-name-name-group show-me-your-name-fifth-name-group";
|
const fifthGroupClasses = "smyn-name-group smyn-fifth-name-group";
|
||||||
const firstNameClasses = "show-me-your-name-name show-me-your-name-first-name";
|
const firstNameClasses = "smyn-name smyn-first-name";
|
||||||
const secondNameClasses = "show-me-your-name-name show-me-your-name-second-name";
|
const secondNameClasses = "smyn-name smyn-second-name";
|
||||||
const thirdNameClasses = "show-me-your-name-name show-me-your-name-third-name";
|
const thirdNameClasses = "smyn-name smyn-third-name";
|
||||||
const fourthNameClasses = "show-me-your-name-name show-me-your-name-fourth-name";
|
const fourthNameClasses = "smyn-name smyn-fourth-name";
|
||||||
const fifthNameClasses = "show-me-your-name-name show-me-your-name-fifth-name";
|
const fifthNameClasses = "smyn-name smyn-fifth-name";
|
||||||
const prefixClasses = "show-me-your-name-affix show-me-your-name-prefix";
|
const prefixClasses = "smyn-affix smyn-prefix";
|
||||||
const suffixClasses = "show-me-your-name-affix show-me-your-name-suffix";
|
const suffixClasses = "smyn-affix smyn-suffix";
|
||||||
|
|
||||||
|
const animationDuration = Math.max(1, 1.5 * (first.name.length / 12));
|
||||||
|
|
||||||
const topLevelStyle = {
|
const topLevelStyle = {
|
||||||
// Allows names to wrap in reaction popouts.
|
// Allows names to wrap in reaction popouts.
|
||||||
...(isReactionsPopout
|
...(isReactionsPopout
|
||||||
? { display: "flex", flexWrap: "wrap", lineHeight: "1.1em", fontSize: "0.9em" }
|
? { display: "flex", flexWrap: "wrap", lineHeight: "1.1em", fontSize: "0.9em" }
|
||||||
: {}),
|
: {}),
|
||||||
...(hasEffect ? effectCSSVars : {})
|
...(hasEffect ? effectCSSVars : {}),
|
||||||
|
"--smyn-gradient-duration": `${animationDuration}s`
|
||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
|
||||||
const nameElement = (
|
const nameElement = (
|
||||||
|
|
@ -630,7 +674,7 @@ function renderUsername(
|
||||||
...topLevelStyle,
|
...topLevelStyle,
|
||||||
...(topRoleStyle?.normal.original || {})
|
...(topRoleStyle?.normal.original || {})
|
||||||
}}
|
}}
|
||||||
className="show-me-your-name-container"
|
className="smyn-container"
|
||||||
>
|
>
|
||||||
{mentionSymbol && <span>{mentionSymbol}</span>}
|
{mentionSymbol && <span>{mentionSymbol}</span>}
|
||||||
{(
|
{(
|
||||||
|
|
@ -667,9 +711,7 @@ function renderUsername(
|
||||||
].map(({ name, dataText, groupClass, nameClass, position }) => name && (
|
].map(({ name, dataText, groupClass, nameClass, position }) => name && (
|
||||||
<span
|
<span
|
||||||
key={position}
|
key={position}
|
||||||
className={SMYNC(groupClass, { [gradientClasses]: shouldGradientGlow && shouldAnimateSecondaryNames })}
|
className={SMYNC(groupClass)}
|
||||||
data-text={shouldGradientGlow && dataText ? dataText : undefined}
|
|
||||||
style={(shouldGradientGlow && shouldAnimateSecondaryNames ? name.style.gradient.animated : undefined) as React.CSSProperties}
|
|
||||||
>
|
>
|
||||||
<span style={affixColor as React.CSSProperties} className={prefixClasses}>
|
<span style={affixColor as React.CSSProperties} className={prefixClasses}>
|
||||||
<span>{nameSeparator}</span>
|
<span>{nameSeparator}</span>
|
||||||
|
|
@ -677,10 +719,12 @@ function renderUsername(
|
||||||
<span
|
<span
|
||||||
// On non-primary names, allow disabling the effects completely, or just their animation & glow.
|
// On non-primary names, allow disabling the effects completely, or just their animation & glow.
|
||||||
className={SMYNC(nameClass, {
|
className={SMYNC(nameClass, {
|
||||||
|
[gradientClasses]: shouldGradientGlow && shouldAnimateSecondaryNames,
|
||||||
"smyn-effect-container": shouldShowEffect && !ignoreGradients,
|
"smyn-effect-container": shouldShowEffect && !ignoreGradients,
|
||||||
[`smyn-effect-${effectType}`]: shouldShowEffect && !ignoreGradients,
|
[`smyn-effect-${effectType}`]: shouldShowEffect && !ignoreGradients,
|
||||||
"smyn-effect-animated": shouldAnimateEffect && shouldAnimateSecondaryNames
|
"smyn-effect-animated": shouldAnimateEffect && shouldAnimateSecondaryNames
|
||||||
})}
|
})}
|
||||||
|
data-text={shouldGradientGlow && dataText ? dataText : undefined}
|
||||||
data-username-with-effects={needsEffectDataAttr && shouldShowEffect && !ignoreGradients ? name.name : undefined}
|
data-username-with-effects={needsEffectDataAttr && shouldShowEffect && !ignoreGradients ? name.name : undefined}
|
||||||
style={{
|
style={{
|
||||||
...(ignoreFonts ? { "font-family": "var(--font-primary)", "letter-spacing": "normal" } : {}),
|
...(ignoreFonts ? { "font-family": "var(--font-primary)", "letter-spacing": "normal" } : {}),
|
||||||
|
|
@ -814,7 +858,7 @@ function CustomNicknameModal({ modalProps, user }: { modalProps: ModalProps; use
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
/>
|
/>
|
||||||
<TextButton
|
<TextButton
|
||||||
className="show-me-your-name-reset-button"
|
className="smyn-reset-button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setValue("");
|
setValue("");
|
||||||
delete customNicknames[user.id];
|
delete customNicknames[user.id];
|
||||||
|
|
@ -826,7 +870,7 @@ function CustomNicknameModal({ modalProps, user }: { modalProps: ModalProps; use
|
||||||
</TextButton>
|
</TextButton>
|
||||||
<div style={{ paddingTop: "10px", flexGrow: 0 }}></div>
|
<div style={{ paddingTop: "10px", flexGrow: 0 }}></div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
<ModalFooter className="show-me-your-name-modal-footer-container">
|
<ModalFooter className="smyn-modal-footer-container">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
|
@ -895,10 +939,15 @@ const settings = definePluginSettings({
|
||||||
default: true,
|
default: true,
|
||||||
description: "Display custom name format in mentions.",
|
description: "Display custom name format in mentions.",
|
||||||
},
|
},
|
||||||
|
typingIndicator: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
description: "Display the first available name listed in your custom name format in the typing indicator.",
|
||||||
|
},
|
||||||
memberList: {
|
memberList: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
default: true,
|
default: true,
|
||||||
description: "Display the first available name listed in your custom name format in the member list and DMs list.",
|
description: "Display the first available name listed in your custom name format in the members list, DMs list, and friends list.",
|
||||||
},
|
},
|
||||||
profilePopout: {
|
profilePopout: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
|
|
@ -973,31 +1022,31 @@ const settings = definePluginSettings({
|
||||||
},
|
},
|
||||||
customNameColor: {
|
customNameColor: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "The color to use for the custom name you assigned a user if it's not the first displayed. Leave blank for default. Accepts hex(a), rgb(a), or hsl(a) input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")",
|
description: "The color to use for the custom name you assigned a user if it's not the first displayed. Leave blank for default. Accepts any valid CSS input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")",
|
||||||
default: "Role-25",
|
default: "Role-25",
|
||||||
isValid: validColor,
|
isValid: validColor,
|
||||||
},
|
},
|
||||||
friendNameColor: {
|
friendNameColor: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "The color to use for a friend's nickname if it's not the first displayed. Leave blank for default. Accepts hex(a), rgb(a), or hsl(a) input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")",
|
description: "The color to use for a friend's nickname if it's not the first displayed. Leave blank for default. Accepts any valid CSS input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")",
|
||||||
default: "Role-25",
|
default: "Role-25",
|
||||||
isValid: validColor,
|
isValid: validColor,
|
||||||
},
|
},
|
||||||
nicknameColor: {
|
nicknameColor: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "The color to use for the nickname if it's not the first displayed. Leave blank for default. Accepts hex(a), rgb(a), or hsl(a) input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")",
|
description: "The color to use for the nickname if it's not the first displayed. Leave blank for default. Accepts any valid CSS input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")",
|
||||||
default: "Role-25",
|
default: "Role-25",
|
||||||
isValid: validColor,
|
isValid: validColor,
|
||||||
},
|
},
|
||||||
displayNameColor: {
|
displayNameColor: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "The color to use for the display name if it's not the first displayed. Leave blank for default. Accepts hex(a), rgb(a), or hsl(a) input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")",
|
description: "The color to use for the display name if it's not the first displayed. Leave blank for default. Accepts any valid CSS input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")",
|
||||||
default: "Role-25",
|
default: "Role-25",
|
||||||
isValid: validColor,
|
isValid: validColor,
|
||||||
},
|
},
|
||||||
usernameColor: {
|
usernameColor: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "The color to use for the username if it's not the first displayed. Leave blank for default. Accepts hex(a), rgb(a), or hsl(a) input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")",
|
description: "The color to use for the username if it's not the first displayed. Leave blank for default. Accepts any valid CSS input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")",
|
||||||
default: "Role-25",
|
default: "Role-25",
|
||||||
isValid: validColor,
|
isValid: validColor,
|
||||||
},
|
},
|
||||||
|
|
@ -1012,7 +1061,7 @@ const settings = definePluginSettings({
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ShowMeYourName",
|
name: "ShowMeYourName",
|
||||||
description: "Display any permutation of custom nicknames, friend nicknames, server nicknames, display names, and usernames in chat.",
|
description: "Display any permutation of custom nicknames, friend nicknames, server nicknames, display names, and usernames in chat.",
|
||||||
authors: [Devs.Rini, Devs.TheKodeToad, EquicordDevs.Etorix, Devs.sadan, Devs.prism],
|
authors: [EquicordDevs.Etorix, Devs.Rini, Devs.TheKodeToad, Devs.sadan, Devs.prism],
|
||||||
tags: ["SMYN", "Nicknames", "Custom Nicknames",],
|
tags: ["SMYN", "Nicknames", "Custom Nicknames",],
|
||||||
isModified: true,
|
isModified: true,
|
||||||
settings,
|
settings,
|
||||||
|
|
@ -1026,8 +1075,8 @@ export default definePlugin({
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Replace names in messages and replies.
|
// Replace names in messages and replies.
|
||||||
match: /(onContextMenu:\i,children:)(.{0,250}?),"data-text":(\i\+\i)/,
|
match: /(?<=colorString:(\i),colorStrings:(\i).{0,900}?)style:.{0,120}?,(onClick:\i,onContextMenu:\i,children:)(.{0,250}?),"data-text":(\i\+\i)/,
|
||||||
replace: "$1$self.getMessageNameElement(arguments[0])??($2),\"data-text\":$self.getMessageNameText(arguments[0])??($3)"
|
replace: "$3$self.getMessageNameElement({...arguments[0],colorString:$1,colorStrings:$2})??($4),\"data-text\":$self.getMessageNameText(arguments[0])??($5)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Pass the message object to the should-animate checker.
|
// Pass the message object to the should-animate checker.
|
||||||
|
|
@ -1036,12 +1085,37 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Replace names in the typing indicator.
|
||||||
|
find: "activityInviteEducationActivity:",
|
||||||
|
replacement: {
|
||||||
|
match: /(?=\i.\i.getName\((\i.guild_id),\i.id,(\i)\))/,
|
||||||
|
replace: "$self.getTypingMemberListProfilesReactionsVoiceNameText({user:$2,type:\"typingIndicator\",guildId:$1})??"
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// Replace names in DMs list.
|
// Replace names in DMs list.
|
||||||
find: "ImpressionNames.DM_LIST_RIGHT_CLICK_MENU_SHOWN",
|
find: "ImpressionNames.DM_LIST_RIGHT_CLICK_MENU_SHOWN",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=getMentionCount\(\i.id\)>0\),\i=)/,
|
match: /(?<=getMentionCount\(\i.id\)>0\),\i=)/,
|
||||||
replace: "$self.getMemberListProfilesReactionsVoiceNameText({...arguments[0],type:\"membersList\"})??"
|
replace: "$self.getTypingMemberListProfilesReactionsVoiceNameText({...arguments[0],type:\"membersList\"})??"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Replace names in the friends list.
|
||||||
|
find: "hasUniqueUsername()}),usernameClass",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=nick:)(\i)/,
|
||||||
|
replace: "$self.getTypingMemberListProfilesReactionsVoiceNameText({user:arguments[0].user,type:\"membersList\"})??$1"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Don't block name style in friends list just
|
||||||
|
// because the name is the same as the username.
|
||||||
|
find: "location:\"DiscordTag\"});",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=forceUsername:(\i),.{0,550}?displayNameStyles:)\i!==\i\?(\i.displayNameStyles):null/,
|
||||||
|
replace: "!$1?$2:null"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1049,7 +1123,7 @@ export default definePlugin({
|
||||||
find: "channel.isSystemDM(),",
|
find: "channel.isSystemDM(),",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=}\);)(return.{0,500}?{text:)(\i,position:"bottom",children:.{0,40}?children:)(\i\?\?\i.\i.getName\(\i\))/,
|
match: /(?<=}\);)(return.{0,500}?{text:)(\i,position:"bottom",children:.{0,40}?children:)(\i\?\?\i.\i.getName\(\i\))/,
|
||||||
replace: "const smynName=arguments[0].channel.recipients.length===1?$self.getMemberListProfilesReactionsVoiceNameText({user:$self.UserStore.getUser(arguments[0].channel.recipients[0]),type:\"profilesPopout\"})??null:null;$1smynName??$2smynName??$3"
|
replace: "const smynName=arguments[0].channel.recipients.length===1?$self.getTypingMemberListProfilesReactionsVoiceNameText({user:$self.UserStore.getUser(arguments[0].channel.recipients[0]),type:\"profilesPopout\"})??null:null;$1smynName??$2smynName??$3"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1090,7 +1164,7 @@ export default definePlugin({
|
||||||
find: "let{colorRoleName:",
|
find: "let{colorRoleName:",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(let{colorRoleName:\i,colorString:\i,colorStrings:\i,)name:(\i)/,
|
match: /(let{colorRoleName:\i,colorString:\i,colorStrings:\i,)name:(\i)/,
|
||||||
replace: "$1showMeYourNameName:$2=$self.getMemberListProfilesReactionsVoiceNameText({...arguments[0],type:\"membersList\"})??(arguments[0].name)"
|
replace: "$1showMeYourNameName:$2=$self.getTypingMemberListProfilesReactionsVoiceNameText({...arguments[0],type:\"membersList\"})??(arguments[0].name)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1098,7 +1172,7 @@ export default definePlugin({
|
||||||
find: "shouldWrap:!0,loop:!0,inProfile:!0",
|
find: "shouldWrap:!0,loop:!0,inProfile:!0",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(tags:\i,)nickname:(\i)/,
|
match: /(tags:\i,)nickname:(\i)/,
|
||||||
replace: "$1showMeYourNameNickname:$2=$self.getMemberListProfilesReactionsVoiceNameText({...arguments[0],type:\"profilesPopout\"})??(arguments[0].nickname)"
|
replace: "$1showMeYourNameNickname:$2=$self.getTypingMemberListProfilesReactionsVoiceNameText({...arguments[0],type:\"profilesPopout\"})??(arguments[0].nickname)"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1109,11 +1183,11 @@ export default definePlugin({
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(displayName:)(\i.\i.getName\(void 0,void 0,\i\))/,
|
match: /(displayName:)(\i.\i.getName\(void 0,void 0,\i\))/,
|
||||||
replace: "$1$self.getMemberListProfilesReactionsVoiceNameText({user:arguments[0].user,guildId:null,type:\"profilesTooltip\"})??($2)"
|
replace: "$1$self.getTypingMemberListProfilesReactionsVoiceNameText({user:arguments[0].user,guildId:null,type:\"profilesTooltip\"})??($2)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(displayName:)(\i.\i.getName\(\i,\i,\i\))/,
|
match: /(displayName:)(\i.\i.getName\(\i,\i,\i\))/,
|
||||||
replace: "$1$self.getMemberListProfilesReactionsVoiceNameText({user:arguments[0].user,guildId:arguments[0].guildId,type:\"profilesTooltip\"})??($2)"
|
replace: "$1$self.getTypingMemberListProfilesReactionsVoiceNameText({user:arguments[0].user,guildId:arguments[0].guildId,type:\"profilesTooltip\"})??($2)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1122,7 +1196,7 @@ export default definePlugin({
|
||||||
find: "reactionTooltip1,",
|
find: "reactionTooltip1,",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(\i.\i.getName\((\i),\i\?\.id,(\i)\))/,
|
match: /(\i.\i.getName\((\i),\i\?\.id,(\i)\))/,
|
||||||
replace: "$self.getMemberListProfilesReactionsVoiceNameText({user:$3,guildId:$2,type:\"reactionsTooltip\"})??($1)"
|
replace: "$self.getTypingMemberListProfilesReactionsVoiceNameText({user:$3,guildId:$2,type:\"reactionsTooltip\"})??($1)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1137,7 +1211,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
// Replace names in reaction popouts.
|
// Replace names in reaction popouts.
|
||||||
match: /(?<=Child,{className:\i.\i,children:)/g,
|
match: /(?<=Child,{className:\i.\i,children:)/g,
|
||||||
replace: "($self.getMemberListProfilesReactionsVoiceNameElement({user:arguments[0].user,guildId:arguments[0].guildId,type:\"reactionsPopout\"}))??"
|
replace: "($self.getTypingMemberListProfilesReactionsVoiceNameElement({user:arguments[0].user,guildId:arguments[0].guildId,type:\"reactionsPopout\"}))??"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1146,16 +1220,33 @@ export default definePlugin({
|
||||||
find: ",connectUserDragSource:",
|
find: ",connectUserDragSource:",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(serverDeaf:\i,)nick:(\i)/,
|
match: /(serverDeaf:\i,)nick:(\i)/,
|
||||||
replace: "$1showMeYourNameVoice:$2=$self.getMemberListProfilesReactionsVoiceNameText({user:arguments[0].user,guildId:arguments[0].channel.guild_id,type:\"voiceChannel\"})??(arguments[0].nick)"
|
replace: "$1showMeYourNameVoice:$2=$self.getTypingMemberListProfilesReactionsVoiceNameText({user:arguments[0].user,guildId:arguments[0].channel.guild_id,type:\"voiceChannel\"})??(arguments[0].nick)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
|
toCSSCache = new Map();
|
||||||
|
toCSSProbe = document.createElement("div");
|
||||||
|
convertToRGBCanvas = document.createElement("canvas");
|
||||||
|
convertToRGBCanvas.width = convertToRGBCanvas.height = 1;
|
||||||
|
convertToRGBCtx = convertToRGBCanvas.getContext("2d", { willReadFrequently: true });
|
||||||
|
convertToRGBCache = new Map();
|
||||||
|
|
||||||
const data = await DataStore.get<CustomNicknameData>("SMYNCustomNicknames");
|
const data = await DataStore.get<CustomNicknameData>("SMYNCustomNicknames");
|
||||||
customNicknames = data ?? {};
|
customNicknames = data ?? {};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
toCSSCache?.clear();
|
||||||
|
toCSSCache = null;
|
||||||
|
toCSSProbe = null;
|
||||||
|
convertToRGBCache?.clear();
|
||||||
|
convertToRGBCache = null;
|
||||||
|
convertToRGBCanvas = null;
|
||||||
|
convertToRGBCtx = null;
|
||||||
|
},
|
||||||
|
|
||||||
contextMenus: {
|
contextMenus: {
|
||||||
"user-context": userContextPatch
|
"user-context": userContextPatch
|
||||||
},
|
},
|
||||||
|
|
@ -1191,6 +1282,6 @@ export default definePlugin({
|
||||||
getMessageNameText,
|
getMessageNameText,
|
||||||
getMessageNameElement,
|
getMessageNameElement,
|
||||||
getMentionNameElement,
|
getMentionNameElement,
|
||||||
getMemberListProfilesReactionsVoiceNameText,
|
getTypingMemberListProfilesReactionsVoiceNameText,
|
||||||
getMemberListProfilesReactionsVoiceNameElement
|
getTypingMemberListProfilesReactionsVoiceNameElement
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,12 @@
|
||||||
|
|
||||||
.smyn-gradient-unset-bg::after {
|
.smyn-gradient-unset-bg::after {
|
||||||
background: unset;
|
background: unset;
|
||||||
|
-webkit-text-fill-color: currentcolor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.smyn-gradient-inherit-bg::after {
|
.smyn-gradient-inherit-bg::after {
|
||||||
background: inherit;
|
background: inherit;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.smyn-gradient-hovered::after {
|
.smyn-gradient-hovered::after {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue