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:
|
||||
disable: false
|
||||
comment_severity_threshold: LOW
|
||||
comment_severity_threshold: MEDIUM
|
||||
max_review_comments: -1
|
||||
pull_request_opened:
|
||||
help: false
|
||||
summary: false
|
||||
code_review: false
|
||||
include_drafts: false
|
||||
|
||||
ignore_patterns:
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "dist/**"
|
||||
- "browser/**"
|
||||
- "packages/**"
|
||||
- "patches/**"
|
||||
- "misc/**"
|
||||
- "scripts/**"
|
||||
- ".vscode/**"
|
||||
- ".github/**"
|
||||
- "*.map"
|
||||
|
|
|
|||
|
|
@ -1,134 +1,382 @@
|
|||
title: Equicord Review Guidelines
|
||||
# Equicord Code Review Style Guide
|
||||
|
||||
rules:
|
||||
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"
|
||||
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.
|
||||
|
||||
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:
|
||||
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}
|
||||
## Plugin Acceptance (Instant Reject)
|
||||
|
||||
pluginInterop:
|
||||
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();
|
||||
If a plugin breaks ANY of these, reject the entire PR. These are non-negotiable:
|
||||
|
||||
reference:
|
||||
types: Channel, Guild, GuildMember, User, Role, Message from @vencord/discord-types
|
||||
components: Paragraph, BaseText, Flex, Button, ErrorCard from @components/
|
||||
settings: use definePluginSettings from @api/Settings, not inline settings object
|
||||
utilities:
|
||||
clipboard: copyToClipboard from @utils/clipboard
|
||||
discord: insertTextIntoChatInputBox, getCurrentChannel, getCurrentGuild, getIntlMessage, openUserProfile, openPrivateChannel, sendMessage, copyWithToast, getUniqueUsername, fetchUserProfile from @utils/discord
|
||||
css: classNameFactory, classNameToSelector from @utils/css
|
||||
misc: classes, sleep, isObject, isObjectEmpty, pluralise, parseUrl, identity from @utils/misc
|
||||
text: formatDuration, formatDurationMs, humanFriendlyJoin, makeCodeblock, toInlineCode, escapeRegExp from @utils/text
|
||||
modal: openModal, closeModal, ModalRoot, ModalHeader, ModalContent, ModalCloseButton from @utils/modal
|
||||
margins: Margins from @utils/margins (Margins.top8, Margins.bottom16, etc.)
|
||||
guards: isTruthy, isNonNullish from @utils/guards
|
||||
web: saveFile, chooseFile from @utils/web
|
||||
webpackCommon:
|
||||
icons: IconUtils from @webpack/common, NEVER hardcode cdn.discordapp.com URLs
|
||||
getUserAvatarURL: IconUtils.getUserAvatarURL(user, canAnimate?, size?)
|
||||
getDefaultAvatarURL: IconUtils.getDefaultAvatarURL(id)
|
||||
getUserBannerURL: IconUtils.getUserBannerURL({ id, banner, canAnimate?, size })
|
||||
getGuildIconURL: IconUtils.getGuildIconURL({ id, icon, size?, canAnimate? })
|
||||
getGuildBannerURL: IconUtils.getGuildBannerURL(guild, canAnimate?)
|
||||
getChannelIconURL: IconUtils.getChannelIconURL({ id, icon })
|
||||
getEmojiURL: IconUtils.getEmojiURL({ id, animated, size })
|
||||
getApplicationIconURL: IconUtils.getApplicationIconURL(data)
|
||||
getGameAssetURL: IconUtils.getGameAssetURL(data)
|
||||
stores: UserStore, GuildStore, ChannelStore, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, PresenceStore, RelationshipStore, MessageStore, EmojiStore, ThemeStore, PermissionStore, VoiceStateStore from @webpack/common
|
||||
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
|
||||
imports:
|
||||
lazy: findByPropsLazy, findStoreLazy, findComponentByCodeLazy, findExportedComponentLazy from @webpack
|
||||
common: useState, useEffect, useCallback, useStateFromStores from @webpack/common
|
||||
neverHardcode:
|
||||
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
|
||||
apiEndpoints: NEVER hardcode "/api/v9", "/users/@me" ⟹ use Constants.Endpoints or RestAPI
|
||||
console: NEVER use console.log/warn/error ⟹ use Logger from @utils/Logger
|
||||
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)
|
||||
1. **No simple slash-command plugins** (e.g. `/cat`). Should be a [user-installable Discord app](https://discord.com/developers/docs/change-log#userinstallable-apps-preview) instead.
|
||||
2. **No simple text replacement plugins.** The built-in TextReplace plugin already covers this.
|
||||
3. **No raw DOM manipulation.** Always use patches and React.
|
||||
4. **No FakeDeafen or FakeMute.**
|
||||
5. **No StereoMic-related plugins.**
|
||||
6. **No UI-only hide/redesign plugins.** Use CSS for that. (Negotiable in rare cases.)
|
||||
7. **No plugins targeting specific third-party Discord bots.** Official Discord apps are fine.
|
||||
8. **No selfbot or API abuse.** Auto-replies, animated statuses, message pruning, Nitro snipers, etc.
|
||||
9. **No untrusted third-party APIs.** Well-known services (Google, GitHub) are acceptable.
|
||||
10. **No plugins requiring users to provide their own API keys.**
|
||||
11. **No new dependencies** unless strictly necessary and well justified.
|
||||
|
||||
---
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
Default export via `definePlugin` from `@utils/types`. Non-negotiable.
|
||||
|
||||
```typescript
|
||||
import definePlugin from "@utils/types";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
|
||||
export default definePlugin({
|
||||
name: "PluginName", // PascalCase, matches directory name
|
||||
description: "Does something.", // Capital first, period at end
|
||||
authors: [EquicordDevs.Name], // EquicordDevs for new, Devs for upstream
|
||||
});
|
||||
```
|
||||
|
||||
**Settings** must use `definePluginSettings` from `@api/Settings`. Reject inline `options` objects.
|
||||
|
||||
```typescript
|
||||
// GOOD
|
||||
const settings = definePluginSettings({
|
||||
myOption: { type: OptionType.BOOLEAN, description: "Enables the feature.", default: true }
|
||||
});
|
||||
export default definePlugin({ settings, /* ... */ });
|
||||
|
||||
// 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: [
|
||||
{
|
||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
||||
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=className:(\i)\.\i,style:\i,)children:\[/,
|
||||
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
|||
},
|
||||
// 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}"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -54,76 +54,77 @@ export default definePlugin({
|
|||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
||||
replacement: {
|
||||
match: /,nameplate:\i,selectedGuildId:(\i),avatarDecoration/,
|
||||
replace: ",nameplate:null,selectedGuildId:$1,avatarDecoration"
|
||||
},
|
||||
predicate: () => settings.store.removeNameplate
|
||||
},
|
||||
{
|
||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
||||
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=#{intl::MUTE}\),)className:\i\.\i,/,
|
||||
replace: ""
|
||||
match: /,nameplate:\i,selectedGuildId:(\i),avatarDecoration/,
|
||||
replace: ",nameplate:null,selectedGuildId:$1,avatarDecoration",
|
||||
predicate: () => settings.store.removeNameplate
|
||||
},
|
||||
{
|
||||
match: /(?<=#{intl::DEAFEN}\),)className:\i\.\i,/,
|
||||
replace: ""
|
||||
match: /hoverText:(\i),forceHover:\i,children:/g,
|
||||
replace: "hoverText:$1,forceHover:!0,children:",
|
||||
predicate: () => settings.store.alwaysShowUsername
|
||||
},
|
||||
{
|
||||
match: /,\(0,\i\.jsxs?\)\(\i\.\i,\{.{0,600}#{intl::ACCOUNT_INPUT_OPTIONS}\)\}\)(?=\])/,
|
||||
replace: ""
|
||||
match: /avatarDecoration:(.{0,70}),size:/,
|
||||
replace: "avatarDecoration:void 0,size:",
|
||||
predicate: () => settings.store.removeAvatarDecoration
|
||||
},
|
||||
{
|
||||
match: /,\(0,\i\.jsxs?\)\(\i\.\i,\{.{0,650}#{intl::ACCOUNT_OUTPUT_OPTIONS}\)\}\)(?=\])/,
|
||||
replace: ""
|
||||
match: /displayNameStyles:(\i),/,
|
||||
replace: "displayNameStyles:void 0,",
|
||||
predicate: () => settings.store.removeUsernameStyles
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.removeAudioMenus
|
||||
},
|
||||
{
|
||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
||||
replacement: {
|
||||
match: /hoverText:(\i),forceHover:\i,children:/g,
|
||||
replace: "hoverText:$1,forceHover:!0,children:"
|
||||
},
|
||||
predicate: () => settings.store.alwaysShowUsername
|
||||
},
|
||||
{
|
||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
||||
find: '"MicrophoneButton"',
|
||||
replacement: [
|
||||
{
|
||||
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\}/,
|
||||
replace: "tooltipText:void 0}"
|
||||
replace: "tooltipText:void 0}",
|
||||
predicate: () => settings.store.removeButtonTooltips
|
||||
},
|
||||
{
|
||||
match: /tooltipText:\i,tooltipPositionKey/,
|
||||
replace: "tooltipText:void 0,tooltipPositionKey"
|
||||
match: /(?<=#{intl::DEAFEN}\),)className:\i\.\i,/,
|
||||
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
|
||||
},
|
||||
{
|
||||
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"',
|
||||
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,"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ function BooIndicator() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{settings.store.showIndicator && getGhostedChannels().length && (
|
||||
{settings.store.showIndicator && getGhostedChannels().length > 0 && (
|
||||
<div id={cl("container")}>
|
||||
<Tooltip text={getTooltipText()} position="right">
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
background-color: var(--background-primary);
|
||||
height: 30px;
|
||||
width: 48px;
|
||||
margin-left: 8px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
color: var(--text-default);
|
||||
fill: var(--text-default);
|
||||
}
|
||||
|
|
@ -153,4 +154,4 @@
|
|||
|
||||
.vc-boo-modal-text {
|
||||
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 { isPluginEnabled } from "@api/PluginManager";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import betterActivities from "@equicordplugins/betterActivities";
|
||||
import showMeYourName from "@plugins/showMeYourName";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
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 { MessageFlags } from "@vencord/discord-types/enums";
|
||||
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-");
|
||||
|
||||
|
|
@ -30,6 +31,14 @@ const ActivityText: React.ComponentType<ActivityTextProps> = findComponentByCode
|
|||
|
||||
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 IconType = AttachmentType | "voice" | "sticker";
|
||||
|
||||
|
|
@ -150,7 +159,7 @@ function MessagePreviewContent({ channel, user }: { channel: Channel; user: User
|
|||
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 (channel.isMultiUserDM()) return <>{channel.recipients.length + 1} Members</>;
|
||||
|
|
@ -231,6 +240,7 @@ export default definePlugin({
|
|||
name: "MessagePeek",
|
||||
description: "Shows the last message preview and timestamp in the Direct Messages list.",
|
||||
authors: [Devs.prism, EquicordDevs.justjxke],
|
||||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: "PrivateChannel.renderAvatar",
|
||||
|
|
@ -252,11 +262,28 @@ export default definePlugin({
|
|||
|
||||
renderMemberListDecorator({ channel }: DecoratorProps) {
|
||||
if (!channel) return null;
|
||||
if (settings.store.hideMuted && UserGuildSettingsStore.isChannelMuted(null!, channel.id)) return null;
|
||||
return <Timestamp channel={channel} />;
|
||||
},
|
||||
|
||||
getSubText(props: PrivateChannelProps) {
|
||||
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 hasActivity = hasRelevantActivity({ activities, status, applicationStream, voiceChannel });
|
||||
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: [
|
||||
{
|
||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
||||
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||
replacement: {
|
||||
// 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, ...
|
||||
replace: "$self.PanelWrapper,{VencordOriginal:$1,"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ type ProfileCollectible = {
|
|||
};
|
||||
|
||||
type ImageInput = string | { imageUri: string; } | null | undefined;
|
||||
type DisplayNameStylesLike = DisplayNameStyles & {
|
||||
fontId?: number;
|
||||
effectId?: number;
|
||||
};
|
||||
|
||||
function dispatch(type: string, payload: Record<string, unknown>) {
|
||||
FluxDispatcher.dispatch({ type, ...payload });
|
||||
|
|
@ -73,6 +77,22 @@ function hasImageInput(value: ImageInput): boolean {
|
|||
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> {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
|
@ -208,12 +228,11 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil
|
|||
type: nameplateToUse.type || 2
|
||||
} : null;
|
||||
|
||||
const displayNameStylesToUse = pendingChanges.pendingDisplayNameStyles ?? userAny.displayNameStyles;
|
||||
const displayNameStyles = displayNameStylesToUse ? {
|
||||
font_id: displayNameStylesToUse.font_id ?? 0,
|
||||
effect_id: displayNameStylesToUse.effect_id ?? 0,
|
||||
colors: Array.isArray(displayNameStylesToUse.colors) ? displayNameStylesToUse.colors : []
|
||||
} : null;
|
||||
const savedDisplayNameStyles = isGuildProfile
|
||||
? (guildMember?.displayNameStyles ?? userAny.displayNameStyles)
|
||||
: userAny.displayNameStyles;
|
||||
const displayNameStylesToUse = pendingChanges.pendingDisplayNameStyles ?? savedDisplayNameStyles;
|
||||
const displayNameStyles = normalizeDisplayNameStyles(displayNameStylesToUse as DisplayNameStylesLike | null | undefined);
|
||||
|
||||
const pendingAvatar = pendingChanges.pendingAvatar ?? pendingChanges.avatar;
|
||||
const avatarToUse: ImageInput = hasImageInput(pendingAvatar)
|
||||
|
|
@ -245,7 +264,7 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil
|
|||
globalName: isGuildProfile
|
||||
? (pendingChanges.pendingNickname ?? guildMember?.nick ?? null)
|
||||
: (pendingChanges.pendingGlobalName ?? currentUser.globalName ?? null),
|
||||
pronouns: pendingChanges.pendingPronouns ?? userProfile?.pronouns ?? null,
|
||||
pronouns: isGuildProfile ? null : (pendingChanges.pendingPronouns ?? userProfile?.pronouns ?? null),
|
||||
avatarDecoration,
|
||||
profileEffect,
|
||||
nameplate,
|
||||
|
|
@ -278,11 +297,16 @@ export async function loadPresetAsPending(preset: ProfilePreset, guildId?: strin
|
|||
const current = await getCurrentProfile(guildId, {
|
||||
isGuildProfile: isGuild
|
||||
});
|
||||
const profilePrefix = "USER_SETTINGS_ACCOUNT";
|
||||
const profilePrefix = isGuild ? "GUILD_IDENTITY_SETTINGS" : "USER_SETTINGS_ACCOUNT";
|
||||
const dispatchProfile = (type: string, payload: Record<string, unknown>) => {
|
||||
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) {
|
||||
const avatarValue = preset.avatarDataUrl;
|
||||
|
|
@ -300,14 +324,14 @@ export async function loadPresetAsPending(preset: ProfilePreset, guildId?: strin
|
|||
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;
|
||||
setPending(`${profilePrefix}_SET_PENDING_PRONOUNS`, { pronouns: pronounsValue });
|
||||
}
|
||||
|
||||
if (!options.skipGlobalName && preset.globalName !== undefined && preset.globalName !== current.globalName) {
|
||||
if (isGuild) {
|
||||
setPending("USER_SETTINGS_ACCOUNT_SET_PENDING_NICKNAME", { nickname: preset.globalName });
|
||||
setPending("GUILD_IDENTITY_SETTINGS_SET_PENDING_NICKNAME", { nickname: preset.globalName });
|
||||
} else {
|
||||
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)) {
|
||||
setPending(`${profilePrefix}_SET_PENDING_DISPLAY_NAME_STYLES`, { displayNameStyles: preset.displayNameStyles });
|
||||
if (preset.displayNameStyles !== undefined) {
|
||||
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)) {
|
||||
|
|
@ -360,4 +387,4 @@ export async function loadPresetAsPending(preset: ProfilePreset, guildId?: strin
|
|||
} catch (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 {
|
||||
if (!quest) { return null; }
|
||||
|
||||
const task = getQuestTask(quest);
|
||||
|
||||
if (!task) { return null; }
|
||||
|
|
@ -1294,8 +1296,8 @@ export default definePlugin({
|
|||
// Fixes the progress tracking for auto-completing Quests.
|
||||
find: ",{progressTextAnimation:",
|
||||
replacement: {
|
||||
match: /(?<=children:\i}=)(\i)/,
|
||||
replace: "Object.assign({},$1,$1.quest?$self.getQuestPanelPercentComplete($1):{})"
|
||||
match: /(let{percentComplete:.{0,115}?children:\i}=)(\i)/,
|
||||
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 {
|
||||
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 {
|
||||
|
|
@ -153,12 +154,31 @@ function calculateTextLines(
|
|||
let currentLine: string[] = [];
|
||||
|
||||
words.forEach(word => {
|
||||
const testLine = [...currentLine, word].join(" ");
|
||||
if (ctx.measureText(testLine).width > maxWidth && currentLine.length) {
|
||||
lines.push(currentLine.join(" "));
|
||||
currentLine = [word];
|
||||
if (ctx.measureText(word).width > maxWidth) {
|
||||
if (currentLine.length) {
|
||||
lines.push(currentLine.join(" "));
|
||||
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 {
|
||||
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",
|
||||
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: {
|
||||
type: OptionType.BOOLEAN,
|
||||
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) {
|
||||
if (!RelationshipStore.isFriend(friendId)) return false;
|
||||
const list = settings.store.allowedFriends.split(",").join(" ").split(" ").filter(i => i.length > 0);
|
||||
if (list.join(" ").length < 1) return true;
|
||||
return list.includes(friendId);
|
||||
const ignoreList = settings.store.ignoredFriends.split(",").join(" ").split(" ").filter(i => i.length > 0);
|
||||
if (ignoreList.includes(friendId)) return false;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export default definePlugin({
|
|||
|
||||
patches: [
|
||||
{
|
||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
||||
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
|
|
@ -87,7 +87,7 @@ export default definePlugin({
|
|||
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)"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export default definePlugin({
|
|||
predicate: () => settings.store.disableFade
|
||||
},
|
||||
{ // Load menu TOC eagerly
|
||||
find: "#{intl::USER_SETTINGS_WITH_BUILD_OVERRIDE}",
|
||||
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||
replacement: {
|
||||
match: /(?=handleOpenSettingsContextMenu=.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)))/,
|
||||
replace: "_vencordBetterSettingsEagerLoad=(async ()=>$1)();"
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export default definePlugin({
|
|||
},
|
||||
// Current user area, at bottom of channels/dm list
|
||||
{
|
||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
||||
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||
replacement: [
|
||||
// Use Decor avatar decoration hook
|
||||
{
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export default definePlugin({
|
|||
{
|
||||
find: "useCanFavoriteChannel",
|
||||
replacement: {
|
||||
match: /(?<=\i\.id\)\),\i=)\i\.isThread\(\)/,
|
||||
match: /(?<=isFavorite\(\i\.id\).{0,5})\i\.isThread\(\)/,
|
||||
replace: "false",
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -461,10 +461,10 @@ export default definePlugin({
|
|||
},
|
||||
},
|
||||
{
|
||||
find: 'className:"channelMention",children:',
|
||||
find: '"channel_mention"});',
|
||||
replacement: {
|
||||
// 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})`
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { Button, TextButton } from "@components/Button";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Heading } from "@components/Heading";
|
||||
import ircColors from "@plugins/ircColors";
|
||||
import mentionAvatars from "@plugins/mentionAvatars";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
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 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 symbolPattern = /^[\p{S}\p{P}]{1,3}$/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>;
|
||||
let customNicknames: CustomNicknameData = {};
|
||||
|
||||
function adjustHex(color: string, percent: number): string {
|
||||
let hex = color.replace("#", "");
|
||||
let toCSSCache: Map<string, string | null> | null = null;
|
||||
let toCSSProbe: HTMLDivElement | null = null;
|
||||
|
||||
if (hex.length === 3) {
|
||||
hex = hex.split("").map(c => c + c).join("");
|
||||
}
|
||||
function toCSS(color: string | number | null | undefined): string | null {
|
||||
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);
|
||||
let r = (num >> 16) & 0xFF;
|
||||
let g = (num >> 8) & 0xFF;
|
||||
let b = num & 0xFF;
|
||||
const cached = toCSSCache?.get(color);
|
||||
if (cached !== undefined) return cached ?? null;
|
||||
|
||||
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))));
|
||||
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))));
|
||||
|
||||
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 color;
|
||||
}
|
||||
|
||||
return `#${newColor}`;
|
||||
return hex;
|
||||
}
|
||||
|
||||
function validColor(color: string) {
|
||||
|
|
@ -73,66 +103,43 @@ function validColor(color: string) {
|
|||
return !isNaN(percentage) && percentage <= 100 && percentage >= -100;
|
||||
}
|
||||
|
||||
const colorTestDiv = document.createElement("div");
|
||||
colorTestDiv.style.borderColor = color;
|
||||
|
||||
const isValid = colorTestDiv.style.borderColor !== "" && colorPattern.test(color);
|
||||
colorTestDiv.remove();
|
||||
|
||||
return isValid;
|
||||
return !!toCSS(color);
|
||||
}
|
||||
|
||||
function resolveColor(
|
||||
user: User | GuildMember,
|
||||
colorStrings: colorStringsType,
|
||||
displayNameStyles: { effectId: number; colors: number[]; } | null | undefined,
|
||||
savedColor: string,
|
||||
canUseGradient: boolean,
|
||||
inGuild: boolean,
|
||||
forceWhite: boolean
|
||||
forceDefault: boolean
|
||||
): Record<string, any> | null {
|
||||
if (!savedColor.trim()) { return null; }
|
||||
|
||||
let gradient: any = null;
|
||||
let primaryColor: any = savedColor;
|
||||
let secondaryColor: any = savedColor;
|
||||
let tertiaryColor: any = savedColor;
|
||||
let primaryAdjusted: any = savedColor;
|
||||
let secondaryAdjusted: any = savedColor;
|
||||
let tertiaryAdjusted: any = savedColor;
|
||||
let primaryColor: any = null;
|
||||
let secondaryColor: any = null;
|
||||
let tertiaryColor: any = null;
|
||||
let primaryAdjusted: any = null;
|
||||
let secondaryAdjusted: any = null;
|
||||
let tertiaryAdjusted: any = null;
|
||||
|
||||
if (savedColor.toLowerCase().includes("role")) {
|
||||
const percentage = roleColorPattern.exec(savedColor)?.[1] || "";
|
||||
if (percentage && isNaN(parseInt(percentage))) return null;
|
||||
|
||||
const colorStrings = (user as any)?.colorStrings || {};
|
||||
const displayStyles = !inGuild ? (user as any)?.displayNameStyles || {} : {};
|
||||
primaryColor = forceDefault ? toCSS("var(--text-strong)") : (toCSS(colorStrings?.primaryColor) || (!inGuild && toCSS(displayNameStyles?.colors?.[0])) || toCSS("var(--text-strong)"));
|
||||
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";
|
||||
if (typeof primaryColor === "number") primaryColor = `#${primaryColor.toString(16).padStart(6, "0")}`;
|
||||
|
||||
secondaryColor = forceWhite ? null : colorStrings.secondaryColor || displayStyles?.colors?.[1] || null;
|
||||
if (typeof secondaryColor === "number") secondaryColor = `#${secondaryColor.toString(16).padStart(6, "0")}`;
|
||||
|
||||
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));
|
||||
}
|
||||
primaryAdjusted = percentage ? adjustBrightness(primaryColor, parseInt(percentage)) : primaryColor;
|
||||
secondaryAdjusted = secondaryColor && percentage ? adjustBrightness(secondaryColor, parseInt(percentage)) : secondaryColor;
|
||||
tertiaryAdjusted = tertiaryColor && percentage ? adjustBrightness(tertiaryColor, parseInt(percentage)) : tertiaryColor;
|
||||
} else {
|
||||
primaryColor = savedColor;
|
||||
}
|
||||
|
||||
gradient = !canUseGradient || !secondaryColor || forceWhite
|
||||
gradient = !canUseGradient || !secondaryColor || forceDefault
|
||||
? null
|
||||
: 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))"
|
||||
|
|
@ -164,7 +171,7 @@ function resolveColor(
|
|||
"--custom-gradient-color-2": secondaryColor || primaryColor,
|
||||
"--custom-gradient-color-3": tertiaryColor || primaryColor,
|
||||
"background-image": gradient,
|
||||
"animation": "smyn-animation 1.5s linear infinite"
|
||||
"animation": "smyn-animation var(--smyn-gradient-duration) linear infinite"
|
||||
},
|
||||
static: {
|
||||
original: {
|
||||
|
|
@ -300,6 +307,8 @@ interface mentionProps {
|
|||
|
||||
interface messageProps {
|
||||
message: Message | null | undefined;
|
||||
colorString?: string;
|
||||
colorStrings: colorStringsType;
|
||||
userOverride?: User;
|
||||
isRepliedMessage?: boolean;
|
||||
withMentionPrefix?: boolean;
|
||||
|
|
@ -307,12 +316,14 @@ interface messageProps {
|
|||
|
||||
interface memberListProfileReactionProps {
|
||||
user: User | null | undefined;
|
||||
type: "membersList" | "profilesPopout" | "profilesTooltip" | "reactionsTooltip" | "reactionsPopout" | "voiceChannel";
|
||||
type: "typingIndicator" | "membersList" | "profilesPopout" | "profilesTooltip" | "reactionsTooltip" | "reactionsPopout" | "voiceChannel";
|
||||
guildId?: string;
|
||||
tags?: any;
|
||||
}
|
||||
|
||||
function getMemberListProfilesReactionsVoiceName(
|
||||
type colorStringsType = { primaryColor: string | null, secondaryColor: string | null, tertiaryColor: string | null; } | null | undefined;
|
||||
|
||||
function getTypingMemberListProfilesReactionsVoiceName(
|
||||
props: memberListProfileReactionProps,
|
||||
): [string | null, JSX.Element | null, string | null] {
|
||||
const { user, type } = props;
|
||||
|
|
@ -321,16 +332,16 @@ function getMemberListProfilesReactionsVoiceName(
|
|||
const guildId = props.guildId || props.tags?.props?.displayProfile?.guildId || null;
|
||||
const member = guildId && user ? GuildMemberStore.getMember(guildId, user.id) : 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);
|
||||
}
|
||||
|
||||
function getMemberListProfilesReactionsVoiceNameText(props: memberListProfileReactionProps): string | null {
|
||||
return getMemberListProfilesReactionsVoiceName(props)[2];
|
||||
function getTypingMemberListProfilesReactionsVoiceNameText(props: memberListProfileReactionProps): string | null {
|
||||
return getTypingMemberListProfilesReactionsVoiceName(props)[2];
|
||||
}
|
||||
|
||||
function getMemberListProfilesReactionsVoiceNameElement(props: memberListProfileReactionProps): JSX.Element | null {
|
||||
return getMemberListProfilesReactionsVoiceName(props)[1];
|
||||
function getTypingMemberListProfilesReactionsVoiceNameElement(props: memberListProfileReactionProps): JSX.Element | null {
|
||||
return getTypingMemberListProfilesReactionsVoiceName(props)[1];
|
||||
}
|
||||
|
||||
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 author = user && member ? { ...user, ...member } : user || member || null;
|
||||
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 {
|
||||
|
|
@ -362,7 +373,20 @@ function getMentionNameElement(props: mentionProps): JSX.Element | null {
|
|||
const member = channel ? GuildMemberStore.getMember(channel.guild_id, userId) : null;
|
||||
const author = user && member ? { ...user, ...member } : user || member || null;
|
||||
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 {
|
||||
|
|
@ -405,14 +429,17 @@ function renderUsername(
|
|||
author: User | GuildMember | null,
|
||||
channelId: 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,
|
||||
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] {
|
||||
const isMessage = type === "messages";
|
||||
const isReply = type === "replies";
|
||||
const isMention = type === "mentions";
|
||||
const isTyping = type === "typingIndicator";
|
||||
const isMember = type === "membersList";
|
||||
const isProfile = type === "profilesPopout";
|
||||
const isReactionsPopout = type === "reactionsPopout";
|
||||
|
|
@ -420,8 +447,8 @@ function renderUsername(
|
|||
const isReaction = isReactionsTooltip || isReactionsPopout;
|
||||
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 { messages, replies, mentions, memberList, profilePopout, reactions, friendNameOnlyInDirectMessages, customNameOnlyInDirectMessages, discriminators, truncateAllNamesWithStreamerMode, removeDuplicates, ignoreGradients, ignoreFonts, animateGradients, includedNames, customNameColor, friendNameColor, nicknameColor, displayNameColor, usernameColor, nameSeparator, triggerNameRerender } = config;
|
||||
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, 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 message = channelId && messageId ? MessageStore.getMessage(channelId, messageId) : null;
|
||||
|
|
@ -435,27 +462,38 @@ function renderUsername(
|
|||
? hoveringReactionPopoutSet.has((author as User).id)
|
||||
: 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 effectCSSVars = authorDisplayNameStyles ? computeEffectCSSVars(authorDisplayNameStyles) : {};
|
||||
const hasEffect = !!effectType;
|
||||
const needsEffectDataAttr = effectType === "neon" || effectType === "toon" || effectType === "pop";
|
||||
const shouldShowEffect = hasEffect && isHovering;
|
||||
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 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 textMutedValue = getComputedStyle(document.documentElement)?.getPropertyValue("--text-muted")?.trim() || "#72767d";
|
||||
const options = splitTemplate(includedNames);
|
||||
const resolvedUsernameColor = author ? resolveColor(author, usernameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||
const resolvedDisplayNameColor = author ? resolveColor(author, displayNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||
const resolvedNicknameColor = author ? resolveColor(author, nicknameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||
const resolvedFriendNameColor = author ? resolveColor(author, friendNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||
const resolvedCustomNameColor = author ? resolveColor(author, customNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||
const resolvedUsernameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, usernameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||
const resolvedDisplayNameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, displayNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||
const resolvedNicknameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, nicknameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
|
||||
const resolvedFriendNameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, friendNameColor.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 { username, display, nick, friend, custom } = getProcessedNames(author, truncateAllNamesWithStreamerMode, discriminators, inGuild, friendNameOnlyInDirectMessages, customNameOnlyInDirectMessages);
|
||||
|
||||
|
|
@ -551,6 +589,8 @@ function renderUsername(
|
|||
return [null, null, null];
|
||||
} else if (isMention && !mentions) {
|
||||
return [null, null, null];
|
||||
} else if (isTyping && !typingIndicator) {
|
||||
return [null, null, null];
|
||||
} else if (isMember && !memberList) {
|
||||
return [null, null, null];
|
||||
} else if (isProfile && !profilePopout) {
|
||||
|
|
@ -580,6 +620,7 @@ function renderUsername(
|
|||
}
|
||||
|
||||
const remainingNames = [first, second, third, fourth, fifth].filter(Boolean);
|
||||
|
||||
first = remainingNames.shift();
|
||||
second = remainingNames.shift();
|
||||
third = remainingNames.shift();
|
||||
|
|
@ -591,37 +632,40 @@ function renderUsername(
|
|||
const shouldAnimateSecondaryNames = animateGradients && !ignoreGradients;
|
||||
|
||||
const firstDataText = mentionSymbol + first.name;
|
||||
const secondDataText = second && shouldAnimateSecondaryNames ? (second.prefix + second.name + second.suffix) : "";
|
||||
const thirdDataText = third && shouldAnimateSecondaryNames ? (third.prefix + third.name + third.suffix) : "";
|
||||
const fourthDataText = fourth && shouldAnimateSecondaryNames ? (fourth.prefix + fourth.name + fourth.suffix) : "";
|
||||
const fifthDataText = fifth && shouldAnimateSecondaryNames ? (fifth.prefix + fifth.name + fifth.suffix) : "";
|
||||
const secondDataText = second && shouldAnimateSecondaryNames ? second.name : "";
|
||||
const thirdDataText = third && shouldAnimateSecondaryNames ? third.name : "";
|
||||
const fourthDataText = fourth && shouldAnimateSecondaryNames ? fourth.name : "";
|
||||
const fifthDataText = fifth && shouldAnimateSecondaryNames ? fifth.name : "";
|
||||
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.
|
||||
const hoveringClass = (isHovering ? " show-me-your-name-gradient-hovered" : "");
|
||||
const hoveringClass = (isHovering ? " smyn-gradient-hovered" : "");
|
||||
const gradientClasses = useTopRoleStyle
|
||||
? "show-me-your-name-gradient show-me-your-name-gradient-inherit-bg" + hoveringClass
|
||||
: "show-me-your-name-gradient show-me-your-name-gradient-unset-bg" + hoveringClass;
|
||||
? "smyn-gradient smyn-gradient-inherit-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 secondGroupClasses = "show-me-your-name-name-group show-me-your-name-second-name-group";
|
||||
const thirdGroupClasses = "show-me-your-name-name-group show-me-your-name-third-name-group";
|
||||
const fourthGroupClasses = "show-me-your-name-name-group show-me-your-name-fourth-name-group";
|
||||
const fifthGroupClasses = "show-me-your-name-name-group show-me-your-name-fifth-name-group";
|
||||
const firstNameClasses = "show-me-your-name-name show-me-your-name-first-name";
|
||||
const secondNameClasses = "show-me-your-name-name show-me-your-name-second-name";
|
||||
const thirdNameClasses = "show-me-your-name-name show-me-your-name-third-name";
|
||||
const fourthNameClasses = "show-me-your-name-name show-me-your-name-fourth-name";
|
||||
const fifthNameClasses = "show-me-your-name-name show-me-your-name-fifth-name";
|
||||
const prefixClasses = "show-me-your-name-affix show-me-your-name-prefix";
|
||||
const suffixClasses = "show-me-your-name-affix show-me-your-name-suffix";
|
||||
const firstGroupClasses = "smyn-name-group smyn-first-name-group";
|
||||
const secondGroupClasses = "smyn-name-group smyn-second-name-group";
|
||||
const thirdGroupClasses = "smyn-name-group smyn-third-name-group";
|
||||
const fourthGroupClasses = "smyn-name-group smyn-fourth-name-group";
|
||||
const fifthGroupClasses = "smyn-name-group smyn-fifth-name-group";
|
||||
const firstNameClasses = "smyn-name smyn-first-name";
|
||||
const secondNameClasses = "smyn-name smyn-second-name";
|
||||
const thirdNameClasses = "smyn-name smyn-third-name";
|
||||
const fourthNameClasses = "smyn-name smyn-fourth-name";
|
||||
const fifthNameClasses = "smyn-name smyn-fifth-name";
|
||||
const prefixClasses = "smyn-affix smyn-prefix";
|
||||
const suffixClasses = "smyn-affix smyn-suffix";
|
||||
|
||||
const animationDuration = Math.max(1, 1.5 * (first.name.length / 12));
|
||||
|
||||
const topLevelStyle = {
|
||||
// Allows names to wrap in reaction popouts.
|
||||
...(isReactionsPopout
|
||||
? { display: "flex", flexWrap: "wrap", lineHeight: "1.1em", fontSize: "0.9em" }
|
||||
: {}),
|
||||
...(hasEffect ? effectCSSVars : {})
|
||||
...(hasEffect ? effectCSSVars : {}),
|
||||
"--smyn-gradient-duration": `${animationDuration}s`
|
||||
} as React.CSSProperties;
|
||||
|
||||
const nameElement = (
|
||||
|
|
@ -630,7 +674,7 @@ function renderUsername(
|
|||
...topLevelStyle,
|
||||
...(topRoleStyle?.normal.original || {})
|
||||
}}
|
||||
className="show-me-your-name-container"
|
||||
className="smyn-container"
|
||||
>
|
||||
{mentionSymbol && <span>{mentionSymbol}</span>}
|
||||
{(
|
||||
|
|
@ -667,9 +711,7 @@ function renderUsername(
|
|||
].map(({ name, dataText, groupClass, nameClass, position }) => name && (
|
||||
<span
|
||||
key={position}
|
||||
className={SMYNC(groupClass, { [gradientClasses]: shouldGradientGlow && shouldAnimateSecondaryNames })}
|
||||
data-text={shouldGradientGlow && dataText ? dataText : undefined}
|
||||
style={(shouldGradientGlow && shouldAnimateSecondaryNames ? name.style.gradient.animated : undefined) as React.CSSProperties}
|
||||
className={SMYNC(groupClass)}
|
||||
>
|
||||
<span style={affixColor as React.CSSProperties} className={prefixClasses}>
|
||||
<span>{nameSeparator}</span>
|
||||
|
|
@ -677,10 +719,12 @@ function renderUsername(
|
|||
<span
|
||||
// On non-primary names, allow disabling the effects completely, or just their animation & glow.
|
||||
className={SMYNC(nameClass, {
|
||||
[gradientClasses]: shouldGradientGlow && shouldAnimateSecondaryNames,
|
||||
"smyn-effect-container": shouldShowEffect && !ignoreGradients,
|
||||
[`smyn-effect-${effectType}`]: shouldShowEffect && !ignoreGradients,
|
||||
"smyn-effect-animated": shouldAnimateEffect && shouldAnimateSecondaryNames
|
||||
})}
|
||||
data-text={shouldGradientGlow && dataText ? dataText : undefined}
|
||||
data-username-with-effects={needsEffectDataAttr && shouldShowEffect && !ignoreGradients ? name.name : undefined}
|
||||
style={{
|
||||
...(ignoreFonts ? { "font-family": "var(--font-primary)", "letter-spacing": "normal" } : {}),
|
||||
|
|
@ -814,7 +858,7 @@ function CustomNicknameModal({ modalProps, user }: { modalProps: ModalProps; use
|
|||
style={{ width: "100%" }}
|
||||
/>
|
||||
<TextButton
|
||||
className="show-me-your-name-reset-button"
|
||||
className="smyn-reset-button"
|
||||
onClick={async () => {
|
||||
setValue("");
|
||||
delete customNicknames[user.id];
|
||||
|
|
@ -826,7 +870,7 @@ function CustomNicknameModal({ modalProps, user }: { modalProps: ModalProps; use
|
|||
</TextButton>
|
||||
<div style={{ paddingTop: "10px", flexGrow: 0 }}></div>
|
||||
</ModalContent>
|
||||
<ModalFooter className="show-me-your-name-modal-footer-container">
|
||||
<ModalFooter className="smyn-modal-footer-container">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={async () => {
|
||||
|
|
@ -895,10 +939,15 @@ const settings = definePluginSettings({
|
|||
default: true,
|
||||
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: {
|
||||
type: OptionType.BOOLEAN,
|
||||
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: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
|
@ -973,31 +1022,31 @@ const settings = definePluginSettings({
|
|||
},
|
||||
customNameColor: {
|
||||
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",
|
||||
isValid: validColor,
|
||||
},
|
||||
friendNameColor: {
|
||||
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",
|
||||
isValid: validColor,
|
||||
},
|
||||
nicknameColor: {
|
||||
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",
|
||||
isValid: validColor,
|
||||
},
|
||||
displayNameColor: {
|
||||
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",
|
||||
isValid: validColor,
|
||||
},
|
||||
usernameColor: {
|
||||
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",
|
||||
isValid: validColor,
|
||||
},
|
||||
|
|
@ -1012,7 +1061,7 @@ const settings = definePluginSettings({
|
|||
export default definePlugin({
|
||||
name: "ShowMeYourName",
|
||||
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",],
|
||||
isModified: true,
|
||||
settings,
|
||||
|
|
@ -1026,8 +1075,8 @@ export default definePlugin({
|
|||
replacement: [
|
||||
{
|
||||
// Replace names in messages and replies.
|
||||
match: /(onContextMenu:\i,children:)(.{0,250}?),"data-text":(\i\+\i)/,
|
||||
replace: "$1$self.getMessageNameElement(arguments[0])??($2),\"data-text\":$self.getMessageNameText(arguments[0])??($3)"
|
||||
match: /(?<=colorString:(\i),colorStrings:(\i).{0,900}?)style:.{0,120}?,(onClick:\i,onContextMenu:\i,children:)(.{0,250}?),"data-text":(\i\+\i)/,
|
||||
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.
|
||||
|
|
@ -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.
|
||||
find: "ImpressionNames.DM_LIST_RIGHT_CLICK_MENU_SHOWN",
|
||||
replacement: {
|
||||
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(),",
|
||||
replacement: {
|
||||
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:",
|
||||
replacement: {
|
||||
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",
|
||||
replacement: {
|
||||
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: [
|
||||
{
|
||||
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\))/,
|
||||
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,",
|
||||
replacement: {
|
||||
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.
|
||||
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:",
|
||||
replacement: {
|
||||
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() {
|
||||
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");
|
||||
customNicknames = data ?? {};
|
||||
},
|
||||
|
||||
stop() {
|
||||
toCSSCache?.clear();
|
||||
toCSSCache = null;
|
||||
toCSSProbe = null;
|
||||
convertToRGBCache?.clear();
|
||||
convertToRGBCache = null;
|
||||
convertToRGBCanvas = null;
|
||||
convertToRGBCtx = null;
|
||||
},
|
||||
|
||||
contextMenus: {
|
||||
"user-context": userContextPatch
|
||||
},
|
||||
|
|
@ -1191,6 +1282,6 @@ export default definePlugin({
|
|||
getMessageNameText,
|
||||
getMessageNameElement,
|
||||
getMentionNameElement,
|
||||
getMemberListProfilesReactionsVoiceNameText,
|
||||
getMemberListProfilesReactionsVoiceNameElement
|
||||
getTypingMemberListProfilesReactionsVoiceNameText,
|
||||
getTypingMemberListProfilesReactionsVoiceNameElement
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,10 +31,12 @@
|
|||
|
||||
.smyn-gradient-unset-bg::after {
|
||||
background: unset;
|
||||
-webkit-text-fill-color: currentcolor;
|
||||
}
|
||||
|
||||
.smyn-gradient-inherit-bg::after {
|
||||
background: inherit;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.smyn-gradient-hovered::after {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue