diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..a19ead5af --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,240 @@ +# AGENTS.md + +**IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning.** When uncertain about APIs, patterns, or existing implementations, read the relevant source files rather than relying on assumptions. + +--- + +## Quick Reference + +| Task | Command/Location | +|------|------------------| +| Install | `pnpm install --frozen-lockfile` | +| Build | `pnpm build` | +| Dev/Watch | `pnpm dev` | +| Lint | `pnpm lint` | +| Type check | `pnpm testTsc` | +| Full test | `pnpm test` | +| New Equicord plugin | `src/equicordplugins/PluginName/index.ts` | +| New Vencord plugin | `src/plugins/PluginName/index.ts` | + +**Key Files Index:** +| Need | File | +|------|------| +| Plugin types | `src/utils/types.ts` | +| Settings API | `src/api/Settings.ts` | +| Constants/devs | `src/utils/constants.ts` | +| CSS utilities | `src/utils/css.ts` | +| Webpack common exports | `src/webpack/common/*.ts` | +| Example plugins | `src/plugins/*/index.ts` or `src/equicordplugins/*/index.ts` | +| Discord types | `@vencord/discord-types` (external) | + +--- + +## Project Overview + +**Equicord** is a Discord client mod forked from [Vencord](https://github.com/Vendicated/Vencord). It modifies the Discord web client by patching webpack modules at runtime to inject custom functionality. + +**Technology Stack:** TypeScript (strict), React, Webpack patching +**Package Manager:** pnpm | **License:** GPL-3.0-or-later + +--- + +## Workflow + +**Before writing code:** +1. Explore relevant source files to understand existing patterns +2. Check the Key Files Index for API/type references +3. Look at similar plugins for examples +4. Then proceed with implementation + +This "explore first, then act" approach produces better results than jumping straight to implementation. + +--- + +## Setup Commands + +| Command | Description | +|---------|-------------| +| `pnpm install --frozen-lockfile` | Install dependencies | +| `pnpm build` | Build project | +| `pnpm buildWeb` | Build for web | +| `pnpm dev` / `pnpm watch` | Start dev server (watch mode) | +| `pnpm watchWeb` | Watch web build | +| `pnpm lint` | Lint code | +| `pnpm lint:fix` | Fix lint issues | +| `pnpm lint-styles` | Lint CSS | +| `pnpm testTsc` | Type check | +| `pnpm test` | Full test suite (builds, type checks, lints, generates plugin JSON) | +| `pnpm generatePluginJson` | Generate plugin list | +| `pnpm generateEquicordPluginJson` | Generate Equicord plugin list | + +**Requirements:** Node.js >= 18, pnpm (see package.json for version) + +--- + +## Code Style + +**TypeScript:** Strict mode. Use inference - don't annotate obvious types. + +**Formatting:** Double quotes, semicolons required, single quotes only for escaping. + +**Patterns:** `?.` optional chaining, `??` nullish coalescing (never `||` for defaults), `const` over `let`, arrow functions, destructuring, template literals, early returns, flat code. + +**Philosophy:** Less code is better. No comments unless requested. Delete dead code, never comment it out. No overengineering or premature abstractions. Trust TypeScript inference. + +**Text:** Plugin/setting descriptions: capital first letter, end with period. Error messages: natural human text, no robotic formatting. + +**Performance:** `Map`/`Set` for lookups, `.find()`/`.some()` not `.filter()[0]`, no spread in loops, `Promise.all()` for parallel async. + +**Hygiene:** Delete dead code. No unused imports. No logging (`console.log`, `Logger`). No empty catch blocks. + +--- + +## Plugin Structure + +**Place Equicord plugins in `src/equicordplugins/PluginName/`, upstream/Vencord plugins in `src/plugins/PluginName/`.** + +```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:** Use `definePluginSettings` from `@api/Settings`. Reject inline `options` objects. + +**Prefer declarative APIs:** `flux`, `contextMenus`, `chatBarButton`, `messagePopoverButton`, `managedStyle` instead of manual registration. + +**Reject deprecated fields:** `renderChatBarButton`, `renderMessagePopoverButton`, `options` + +**Lifecycle:** Anything registered in `start()` must be cleaned up in `stop()`. + +--- + +## Forbidden Patterns + +| Bad | Good | Reason | +|-----|------|--------| +| `value !== null && value !== undefined` | `value` or `isNonNullish(value)` | Verbose | +| `array && array.length > 0` | `array.length` | Redundant | +| `settings?.store?.value` | `settings.store.value` | Store always defined | +| `value \|\| defaultValue` | `value ?? defaultValue` | `\|\|` falsifies `0`, `""`, `false` | +| `` `${classA} ${classB}` `` | `classes(classA, classB)` | Handles null/false | +| `"vc-plugin-class"` hardcoded | `cl("class")` via `classNameFactory` | Typo-proof | +| `console.log/warn/error` | Remove it | No logging | +| `cdn.discordapp.com/...` | `IconUtils.*` methods | Handles animated, sizing | +| `/api/v9/...` | `Constants.Endpoints.*` | Endpoints change | +| `@api/Styles` | `@utils/css` | Deprecated | +| `any` for Discord objects | Import from `@vencord/discord-types` | Type safety | +| `as unknown as` | Find correct type | Unsafe | +| `React.memo()` | Remove it | Not needed | +| `React.cloneElement/isValidElement/Children` | Find another approach | Forbidden | +| `React.lazy(() => import(...))` | `LazyComponent` | Framework-integrated | +| Empty `catch {}` | Handle error | Silent failures | +| CSS-only plugins | Must have logic/patches | Not allowed | +| `document.querySelector(...)` | Use webpack patches | DOM forbidden | +| `Vencord.Plugins.plugins["X"]` | `isPluginEnabled` + import | Proper interop | +| `plugin.started` | `isPluginEnabled(plugin.name)` | Proper interop | +| Unexplained magic numbers | Named constants | Readability | +| Unused imports | Remove them | Cleanliness | + +--- + +## CSS, React, Settings + +**CSS:** Use `classNameFactory` from `@utils/css`: `const cl = classNameFactory("vc-my-plugin-");` Combine with `classes()` from `@utils/misc`. + +**React:** Return `null` for conditional rendering, never `undefined`. Wrap complex components with `ErrorBoundary.wrap()`. useEffect must return cleanup for subscriptions/timers. + +**Settings:** `settings.store.key` (reactive), `settings.plain.key` (non-reactive). Never `settings.use()` with arrays in variables. + +--- + +## 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.** Use `\i`, never `e`, `t`, `n`, `r`, `i`, `eD`, `eH`. +- **Bounded gaps.** `.{0,50}` not `.+?` or `.*?`. +- **No generic patterns.** Add stable anchors. +- **No raw intl hashes.** Use `#{intl::KEY_NAME}`. +- **`$&`** for append/prepend, **`$self`** for plugin method calls. + +--- + +## Memory Leaks + +**Every `start()` must have matching `stop()`.** Common leaks: event listeners, intervals/timeouts, MutationObservers, context menu patches, chat bar buttons, styles, Flux subscriptions. + +**Prefer declarative APIs** that handle cleanup automatically: `flux`, `contextMenus`, `chatBarButton`, `messagePopoverButton`, `managedStyle`. + +**useEffect must return cleanup** for subscriptions, timers, observers, or any persistent resource. + +--- + +## Plugin Interop + +- `@equicordplugins/pluginName` for Equicord plugins +- `@plugins/pluginName` for Vencord/upstream plugins + +Use `isPluginEnabled(plugin.name)` to check if enabled. Never access `Vencord.Plugins.plugins["X"]` directly. + +--- + +## Platform-Specific Plugins + +Directory suffixes: `pluginName.desktop/`, `pluginName.web/`, `pluginName.discordDesktop/` + +--- + +## Built-in Utilities + +**@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`, 30+ more), Actions (`RestAPI`, `FluxDispatcher`, `MessageActions`, `NavigationRouter`), Utils (`Constants.Endpoints`, `SnowflakeUtils`, `Parser`, `PermissionsBits`, `moment`, `lodash`, `IconUtils`), Components (`Tooltip`, `TextInput`, `Select`, `Avatar`, `Menu`, `Popout`), Toasts (`Toasts`, `showToast`) + +**@webpack finders:** `findByPropsLazy`, `findByCodeLazy`, `findStoreLazy`, `findComponentByCodeLazy`, `findExportedComponentLazy` + +**@components/:** `ErrorBoundary`, `Flex`, `Button`, `Paragraph`, `Heading`, `BaseText`, `Span`, `ErrorCard`, `Link`, `CodeBlock`, `FormSwitch` + +--- + +## Ethical Guidelines & Acceptance Rules + +**Not accepted:** Fake deafen/mute, trolling plugins, selfbot/API abuse (auto-replies, animated statuses, message pruning, Nitro snipers). + +**Instant reject:** Simple slash-commands, text replacement, DOM manipulation, UI-only hide/redesign, third-party bot targeting, untrusted APIs, user-provided API keys, new dependencies without justification. + +--- + +## Git Workflow + +- Never commit to `main` (protected) +- Never commit to fork's `dev` branch +- Create feature branch from `dev`: `feature/plugin-name` or `fix/plugin-name` +- Keep commits atomic +- Squash commits before merging if addressing same issue +- Run `pnpm lint` and `pnpm build` before committing + +**PR title format:** `[PluginName] Description` + +--- diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/packages/discord-types/src/common/Channel.d.ts b/packages/discord-types/src/common/Channel.d.ts index 6ae5ba895..21f2dc10e 100644 --- a/packages/discord-types/src/common/Channel.d.ts +++ b/packages/discord-types/src/common/Channel.d.ts @@ -14,15 +14,15 @@ export interface PermissionOverwrite { export interface AvatarDecorationData { asset: string; expires_at: string | null; - sku_id: string; + skuId: string; } /** Clan identity information. */ export interface ClanData { badge: string; - identity_enabled: boolean; - identity_guild_id: string; tag: string; + identityEnabled: boolean; + identityGuildId: string; } /** Display name style configuration. */ @@ -38,7 +38,8 @@ export interface CollectibleItem { expires_at: string | null; label: string; palette: string; - sku_id: string; + type: number; + skuId: string; } /** User collectibles configuration. */ diff --git a/packages/discord-types/src/common/User.d.ts b/packages/discord-types/src/common/User.d.ts index 0905b8bf2..f3007f120 100644 --- a/packages/discord-types/src/common/User.d.ts +++ b/packages/discord-types/src/common/User.d.ts @@ -39,7 +39,7 @@ export class User extends DiscordRecord { get avatarDecoration(): AvatarDecorationData | null; get createdAt(): Date; get isProvisional(): boolean; - get nameplate(): { asset: string; sku_id: string; } | null; + get nameplate(): { asset: string; skuId: string; } | null; get premiumGroupRole(): { id: string; name: string; color: number; } | null; get tag(): string; diff --git a/src/equicordplugins/_api/userArea.ts b/src/equicordplugins/_api/userArea.ts index 6f746ff86..9ed35d0c8 100644 --- a/src/equicordplugins/_api/userArea.ts +++ b/src/equicordplugins/_api/userArea.ts @@ -19,7 +19,7 @@ export default definePlugin({ patches: [ { - find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));", + find: ".WIDGETS_RTC_UPSELL_COACHMARK),", replacement: [ { match: /(?<=className:(\i)\.\i,style:\i,)children:\[/, diff --git a/src/equicordplugins/equicordHelper/index.tsx b/src/equicordplugins/equicordHelper/index.tsx index 5459e8edb..59cce166f 100644 --- a/src/equicordplugins/equicordHelper/index.tsx +++ b/src/equicordplugins/equicordHelper/index.tsx @@ -97,15 +97,21 @@ const settings = definePluginSettings({ accountStandingButton: { type: OptionType.BOOLEAN, description: "Show an account standing button in the header bar", - default: false, restartNeeded: true, - } + default: false, + }, + restoreFileDownloadButton: { + type: OptionType.BOOLEAN, + description: "Adds back the Download button at the top right corner of files", + restartNeeded: true, + default: false + }, }); export default definePlugin({ name: "EquicordHelper", description: "Used to provide support, fix discord caused crashes, and other misc features.", - authors: [Devs.thororen, EquicordDevs.nyx, EquicordDevs.Naibuu, EquicordDevs.keircn, EquicordDevs.SerStars, EquicordDevs.mart], + authors: [Devs.thororen, EquicordDevs.nyx, EquicordDevs.Naibuu, EquicordDevs.keircn, EquicordDevs.SerStars, EquicordDevs.mart, EquicordDevs.omaw], required: true, settings, headerBarButton: { @@ -164,6 +170,7 @@ export default definePlugin({ replace: "true||$&" }, }, + // Show your own activity buttons because discord removes them for who knows why { find: ".USER_PROFILE_ACTIVITY_BUTTONS),", predicate: () => settings.store.showYourOwnActivityButtons && !isPluginEnabled(customRPC.name), @@ -200,6 +207,15 @@ export default definePlugin({ replace: "1" } }, + // Restore File Download Button + { + predicate: () => settings.store.restoreFileDownloadButton, + find: '"VISUAL_PLACEHOLDER":', + replacement: { + match: /(\.downloadUrl,showDownload:)\i/, + replace: "$1!0" + } + }, ], renderMessageAccessory(props) { return ( diff --git a/src/equicordplugins/musicControls/index.tsx b/src/equicordplugins/musicControls/index.tsx index a46c3073f..0054b321b 100644 --- a/src/equicordplugins/musicControls/index.tsx +++ b/src/equicordplugins/musicControls/index.tsx @@ -52,10 +52,10 @@ export default definePlugin({ patches: [ { - find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));", + find: ".WIDGETS_RTC_UPSELL_COACHMARK),", replacement: { // react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah }) - match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,(occluded|hidePrivateData):)/, + match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,occluded:)/, // react.jsx(WrapperComponent, { VencordOriginal: AccountPanel, ... replace: "$self.PanelWrapper,{VencordOriginal:$1," }, diff --git a/src/equicordplugins/profileSets/components/presetList.tsx b/src/equicordplugins/profileSets/components/presetList.tsx index 2beb5bc9e..61bf18ce6 100644 --- a/src/equicordplugins/profileSets/components/presetList.tsx +++ b/src/equicordplugins/profileSets/components/presetList.tsx @@ -4,7 +4,9 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { isNonNullish } from "@utils/guards"; import { classes } from "@utils/misc"; +import { ProfilePreset } from "@vencord/discord-types"; import { ContextMenuApi, Menu, React, TextInput } from "@webpack/common"; import { cl } from ".."; @@ -37,6 +39,7 @@ export function PresetList({ currentPage, onPageChange }: PresetListProps) { + type EditableProfile = Omit; const [renaming, setRenaming] = React.useState(-1); const [renameText, setRenameText] = React.useState(""); const isGuildProfile = section === "server"; @@ -149,11 +152,11 @@ export function PresetList({ label="Update" action={async () => { const profile = await getCurrentProfile(guildId, { isGuildProfile }); - Object.entries(profile).forEach(([key, value]) => { - if (value !== null && value !== undefined) { - updatePresetField(actualIndex, key as any, value, section, guildId); - } - }); + await Promise.all( + (Object.entries(profile) as [keyof EditableProfile, EditableProfile[keyof EditableProfile]][]) + .filter(([, value]) => isNonNullish(value)) + .map(([key, value]) => updatePresetField(actualIndex, key, value, section, guildId)) + ); onUpdate(); }} /> diff --git a/src/equicordplugins/profileSets/components/presetManager.tsx b/src/equicordplugins/profileSets/components/presetManager.tsx index b50090720..74f2c5bc8 100644 --- a/src/equicordplugins/profileSets/components/presetManager.tsx +++ b/src/equicordplugins/profileSets/components/presetManager.tsx @@ -137,7 +137,7 @@ export function PresetManager({ section, guildId }: PresetManagerProps) { }); }; - const avatarSize = settings.store.avatarSize || 40; + const { avatarSize } = settings.store; const hasPresets = presets.length > 0; const shouldShowPagination = filteredPresets.length > PRESETS_PER_PAGE; diff --git a/src/equicordplugins/profileSets/index.tsx b/src/equicordplugins/profileSets/index.tsx index dccb74a8c..a2db97aa4 100644 --- a/src/equicordplugins/profileSets/index.tsx +++ b/src/equicordplugins/profileSets/index.tsx @@ -18,9 +18,11 @@ import { loadPresets, PresetSection } from "./utils/storage"; export const cl = classNameFactory("vc-profile-presets-"); export const settings = definePluginSettings({ avatarSize: { - type: OptionType.NUMBER, + type: OptionType.SLIDER, description: "Avatar size in preset list.", - default: 40, + markers: [56, 64, 72, 80, 88, 96], + default: 56, + stickToMarkers: true }, }); @@ -40,8 +42,8 @@ export default definePlugin({ { find: "USER_SETTINGS_GUILD_PROFILE)", replacement: { - match: /onChange:\i\}\)(?=.{0,25}profilePreviewTitle:)/, - replace: '$&,$self.renderPresetSection("server")' + match: /guildId:(\i\.id),onChange:(\i)\}\)(?=.{0,25}profilePreviewTitle:)/, + replace: 'guildId:$1,onChange:$2}),$self.renderPresetSection("server",$1)' } } ], diff --git a/src/equicordplugins/profileSets/utils/actions.ts b/src/equicordplugins/profileSets/utils/actions.ts index eef4b916d..a43aa3e24 100644 --- a/src/equicordplugins/profileSets/utils/actions.ts +++ b/src/equicordplugins/profileSets/utils/actions.ts @@ -4,24 +4,64 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { isNonNullish } from "@utils/guards"; import { ProfilePreset } from "@vencord/discord-types"; +import { findStoreLazy } from "@webpack"; +import { showToast, Toasts } from "@webpack/common"; import { getCurrentProfile } from "./profile"; -import { addPreset, movePresetInArray, PresetSection, presets, removePreset, replaceAllPresets, savePresetsData, updatePreset, type ProfilePresetEx } from "./storage"; +import { addPreset, movePresetInArray, presets, PresetSection, type ProfilePresetEx, removePreset, replaceAllPresets, savePresetsData, updatePreset } from "./storage"; + +const UserProfileSettingsStore = findStoreLazy("UserProfileSettingsStore"); + +function isImageInput(value: unknown): value is string | { imageUri: string; } { + if (typeof value === "string") return value.length > 0; + return typeof value === "object" && isNonNullish(value) && "imageUri" in value && typeof (value as { imageUri: unknown }).imageUri === "string"; +} + +function getFreshPendingAvatar(section: PresetSection, guildId?: string): string | null { + const pending = (section === "server" && guildId + ? UserProfileSettingsStore.getPendingChanges?.(guildId) + : UserProfileSettingsStore.getPendingChanges?.()) ?? {}; + const pendingObj = pending as Record; + + const candidates = [ + pendingObj.selectedAvatarRaw, + pendingObj.presetAvatarRaw, + pendingObj.pendingAvatar, + pendingObj.avatar, + pendingObj.selectedAvatarProcessed, + pendingObj.presetAvatarProcessed + ]; + const selected = candidates.find(isImageInput); + if (!selected) return null; + return typeof selected === "string" ? selected : selected.imageUri; +} export async function savePreset(name: string, section: PresetSection, guildId?: string) { const profile = await getCurrentProfile(guildId, { isGuildProfile: section === "server" }); + const freshPendingAvatar = getFreshPendingAvatar(section, guildId); + const effectiveAvatar = freshPendingAvatar ?? profile.avatarDataUrl ?? null; + const newPreset: ProfilePresetEx = { name, timestamp: Date.now(), ...profile, + avatarDataUrl: effectiveAvatar, }; addPreset(newPreset); await savePresetsData(section); } -export async function updatePresetField(index: number, field: keyof Omit, value: any, section: PresetSection, guildId?: string) { +export async function updatePresetField>( + index: number, + field: K, + value: Omit[K], + section: PresetSection, + guildId?: string +) { if (index < 0 || index >= presets.length) return; + void guildId; const updatedPreset = { ...presets[index], @@ -55,17 +95,14 @@ export async function renamePreset(index: number, newName: string, section: Pres } export function exportPresets(section: PresetSection) { - try { - const dataStr = JSON.stringify(presets, null, 2); - const dataBlob = new Blob([dataStr], { type: "application/json" }); - const url = URL.createObjectURL(dataBlob); - const link = document.createElement("a"); - link.href = url; - link.download = `profile-presets-${section}-${Date.now()}.json`; - link.click(); - URL.revokeObjectURL(url); - } catch { - } + const dataStr = JSON.stringify(presets, null, 2); + const dataBlob = new Blob([dataStr], { type: "application/json" }); + const url = URL.createObjectURL(dataBlob); + const link = document.createElement("a"); + link.href = url; + link.download = `profile-presets-${section}-${Date.now()}.json`; + link.click(); + URL.revokeObjectURL(url); } export type ImportDecision = "override" | "merge" | "cancel"; @@ -108,6 +145,7 @@ export async function importPresets( await savePresetsData(section); forceUpdate(); } catch { + showToast("Failed to import presets. The file might be invalid.", Toasts.Type.FAILURE); } }; input.click(); diff --git a/src/equicordplugins/profileSets/utils/profile.ts b/src/equicordplugins/profileSets/utils/profile.ts index 69d4dcd84..5b7a7bbc7 100644 --- a/src/equicordplugins/profileSets/utils/profile.ts +++ b/src/equicordplugins/profileSets/utils/profile.ts @@ -7,12 +7,16 @@ import { getUserSettingLazy } from "@api/UserSettings"; import { CustomStatus, DisplayNameStyles, Nameplate, ProfileEffect, ProfilePreset } from "@vencord/discord-types"; import { findStoreLazy } from "@webpack"; -import { FluxDispatcher, GuildMemberStore,IconUtils, UserProfileStore, UserStore } from "@webpack/common"; +import { FluxDispatcher, GuildMemberStore, IconUtils, UserProfileStore, UserStore } from "@webpack/common"; const UserProfileSettingsStore = findStoreLazy("UserProfileSettingsStore"); const CustomStatusSettings = getUserSettingLazy("status", "customStatus")!; type PendingChanges = Record & { + selectedAvatarRaw?: ImageInput; + presetAvatarRaw?: ImageInput; + selectedAvatarProcessed?: ImageInput; + presetAvatarProcessed?: ImageInput; pendingAvatar?: ImageInput; avatar?: ImageInput; pendingBanner?: ImageInput; @@ -30,39 +34,23 @@ type PendingChanges = Record & { pendingPrimaryGuildId?: string | null; }; -type UserExtras = { - collectibles?: { - nameplate?: Nameplate; - }; - displayNameStyles?: DisplayNameStyles; - avatarDecorationData?: { - asset: string; - skuId: string; - } | null; - primaryGuild?: { - identityGuildId?: string | null; - } | null; -}; - -type ProfileCollectible = { - skuId: string; - title?: string; - description?: string; - accessibilityLabel?: string; - reducedMotionSrc?: string; - thumbnailPreviewSrc?: string; - effects?: ProfileEffect["effects"]; - animationType?: number; - staticFrameSrc?: string; - type?: number; -}; - type ImageInput = string | { imageUri: string; } | null | undefined; type DisplayNameStylesLike = DisplayNameStyles & { fontId?: number; effectId?: number; }; +type CurrentProfileOptions = { + isGuildProfile?: boolean; +}; + +type LoadPresetOptions = { + skipGlobalName?: boolean; + skipBio?: boolean; + skipPronouns?: boolean; + isGuildProfile?: boolean; +}; + function dispatch(type: string, payload: Record) { FluxDispatcher.dispatch({ type, ...payload }); } @@ -74,7 +62,7 @@ function isNonEmptyString(value: unknown): value is string { function hasImageInput(value: ImageInput): boolean { if (!value) return false; if (typeof value === "string") return value.length > 0; - return typeof value === "object" && "imageUri" in value && isNonEmptyString(value.imageUri); + return typeof value === "object" && isNonEmptyString(value?.imageUri); } function normalizeDisplayNameStyles(value: DisplayNameStylesLike | null | undefined): DisplayNameStylesLike | null { @@ -111,7 +99,7 @@ export async function imageUrlToBase64(url: string): Promise { async function processImage(imageData: ImageInput, userId: string, type: "avatar" | "banner", guildId?: string, useGuildPath?: boolean): Promise { if (!imageData) return null; - if (typeof imageData === "object" && "imageUri" in imageData && isNonEmptyString(imageData.imageUri)) { + if (typeof imageData === "object" && isNonEmptyString(imageData?.imageUri)) { return imageData.imageUri; } @@ -137,17 +125,6 @@ async function processImage(imageData: ImageInput, userId: string, type: "avatar return null; } -type CurrentProfileOptions = { - isGuildProfile?: boolean; -}; - -type LoadPresetOptions = { - skipGlobalName?: boolean; - skipBio?: boolean; - skipPronouns?: boolean; - isGuildProfile?: boolean; -}; - export async function getCurrentProfile(guildId?: string, options: CurrentProfileOptions = {}): Promise> { const currentUser = UserStore.getCurrentUser(); const baseProfile = UserProfileStore.getUserProfile(currentUser.id); @@ -155,19 +132,17 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil const effectiveGuildId = isGuildProfile ? guildId : undefined; const guildProfile = effectiveGuildId ? UserProfileStore.getGuildMemberProfile(currentUser.id, effectiveGuildId) : null; const userProfile = guildProfile ?? baseProfile; - const userAny = currentUser as UserExtras; + const userAny = currentUser; const guildMember = effectiveGuildId ? GuildMemberStore.getMember(effectiveGuildId, currentUser.id) : null; - const pendingChangesDefault = (UserProfileSettingsStore.getPendingChanges() ?? {}) as PendingChanges; - const pendingChangesForGuild = (effectiveGuildId - ? (UserProfileSettingsStore.getPendingChanges(effectiveGuildId) ?? {}) - : {}) as PendingChanges; + const pendingChangesDefault: PendingChanges = UserProfileSettingsStore.getPendingChanges() ?? {}; + const pendingChangesForGuild: PendingChanges = UserProfileSettingsStore.getPendingChanges(effectiveGuildId) ?? {}; const pendingChanges: PendingChanges = isGuildProfile && Object.keys(pendingChangesForGuild).length > 0 ? pendingChangesForGuild : pendingChangesDefault; const customStatusSetting = CustomStatusSettings.getSetting(); - const customStatus: CustomStatus | null = isGuildProfile + const customStatus = isGuildProfile ? null : { text: customStatusSetting?.text ?? "", @@ -200,7 +175,7 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil type: effectToUse.type || 1 }; } else if (effectToUse.skuId) { - const collectibles = (userProfile as { collectibles?: ProfileCollectible[]; } | null)?.collectibles; + const collectibles = userProfile?.collectibles; const collectible = collectibles?.find(c => c?.skuId === effectToUse.skuId); if (collectible) { profileEffect = { @@ -232,7 +207,7 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil ? (guildMember?.displayNameStyles ?? userAny.displayNameStyles) : userAny.displayNameStyles; const displayNameStylesToUse = pendingChanges.pendingDisplayNameStyles ?? savedDisplayNameStyles; - const displayNameStyles = normalizeDisplayNameStyles(displayNameStylesToUse as DisplayNameStylesLike | null | undefined); + const displayNameStyles = normalizeDisplayNameStyles(displayNameStylesToUse); const pendingAvatar = pendingChanges.pendingAvatar ?? pendingChanges.avatar; const avatarToUse: ImageInput = hasImageInput(pendingAvatar) @@ -282,109 +257,159 @@ function jsonEq(a: unknown, b: unknown): boolean { } function customStatusEq(a: CustomStatus | null | undefined, b: CustomStatus | null | undefined): boolean { - if (a == null && b == null) return true; - if (a == null || b == null) return false; + if (a == null || b == null) return a == null && b == null; return a.text === b.text && String(a.emojiId ?? "") === String(b.emojiId ?? "") && a.emojiName === b.emojiName && String(a.expiresAtMs ?? "0") === String(b.expiresAtMs ?? "0"); } +function resolvePendingAvatar(pendingChanges: PendingChanges | null): ImageInput { + if (!pendingChanges) return null; + + const candidates: ImageInput[] = [ + pendingChanges.selectedAvatarRaw, + pendingChanges.presetAvatarRaw, + pendingChanges.pendingAvatar, + pendingChanges.avatar, + pendingChanges.selectedAvatarProcessed, + pendingChanges.presetAvatarProcessed + ]; + return candidates.find(hasImageInput) ?? null; +} + +function normalizeImageValue(value: unknown): string | null { + if (typeof value === "string") return value; + if (value && typeof value === "object" && "imageUri" in value) { + const { imageUri } = value as { imageUri: unknown }; + return typeof imageUri === "string" ? imageUri : null; + } + return null; +} + +function collectibleEqBySku(a: { skuId?: string | number | null; } | null | undefined, b: { skuId?: string | number | null; } | null | undefined): boolean { + if (a == null || b == null) return a == null && b == null; + return String(a.skuId ?? "") === String(b.skuId ?? ""); +} + +function avatarDecorationEq(a: { skuId?: string | number | null; asset?: string | null; } | null | undefined, b: { skuId?: string | number | null; asset?: string | null; } | null | undefined): boolean { + if (a == null || b == null) return a == null && b == null; + return String(a.skuId ?? "") === String(b.skuId ?? "") && String(a.asset ?? "") === String(b.asset ?? ""); +} + +function nameplateEq(a: { skuId?: string | number | null; asset?: string | null; } | null | undefined, b: { skuId?: string | number | null; asset?: string | null; } | null | undefined): boolean { + if (a == null || b == null) return a == null && b == null; + return String(a.skuId ?? "") === String(b.skuId ?? "") && String(a.asset ?? "") === String(b.asset ?? ""); +} + export async function loadPresetAsPending(preset: ProfilePreset, guildId?: string, options: LoadPresetOptions = {}) { try { const isGuild = options.isGuildProfile ?? Boolean(guildId); if (isGuild && !guildId) return; + const currentUser = UserStore.getCurrentUser(); + const baseProfile = UserProfileStore.getUserProfile(currentUser.id); + const guildProfile = isGuild && guildId ? UserProfileStore.getGuildMemberProfile(currentUser.id, guildId) : null; + const equippedProfile = guildProfile ?? baseProfile; const current = await getCurrentProfile(guildId, { isGuildProfile: isGuild }); - const profilePrefix = isGuild ? "GUILD_IDENTITY_SETTINGS" : "USER_SETTINGS_ACCOUNT"; - const dispatchProfile = (type: string, payload: Record) => { - dispatch(type, isGuild ? { ...payload, guildId } : payload); - }; - const setPending = (type: string, payload: Record) => { - dispatchProfile(type, payload); - if (isGuild && type.startsWith("GUILD_IDENTITY_SETTINGS_")) { - dispatchProfile(type.replace("GUILD_IDENTITY_SETTINGS_", "USER_SETTINGS_ACCOUNT_"), payload); - } + const pendingChanges = (isGuild && guildId + ? UserProfileSettingsStore.getPendingChanges(guildId) + : UserProfileSettingsStore.getPendingChanges()); + const equippedAvatarDecoration = currentUser.avatarDecorationData ?? null; + const equippedProfileEffect = equippedProfile?.profileEffect ?? null; + const equippedNameplate = currentUser.collectibles?.nameplate ?? null; + const pendingAvatarDecoration = pendingChanges?.pendingAvatarDecoration ?? null; + const pendingProfileEffect = pendingChanges?.pendingProfileEffect ?? null; + const pendingNameplate = pendingChanges?.pendingNameplate ?? null; + const setPending = (field: string, payload: Record) => { + const cleanPayload = Object.fromEntries(Object.entries(payload).filter(([, v]) => v !== undefined)); + if (!Object.keys(cleanPayload).length) return; + dispatch(`USER_PROFILE_SETTINGS_SET_PENDING_${field}`, isGuild ? { ...cleanPayload, guildId } : cleanPayload); }; if ("avatarDataUrl" in preset) { const avatarValue = preset.avatarDataUrl; - if (isGuild || avatarValue !== current.avatarDataUrl) { - setPending(`${profilePrefix}_SET_PENDING_AVATAR`, { avatar: avatarValue }); + const presetAvatar = normalizeImageValue(avatarValue); + const currentAvatar = normalizeImageValue(current.avatarDataUrl); + const pendingAvatar = normalizeImageValue(resolvePendingAvatar(pendingChanges)); + if (isGuild || (presetAvatar !== currentAvatar && presetAvatar !== pendingAvatar)) { + const avatarPayload = + typeof avatarValue === "string" && avatarValue.startsWith("data:") + ? { + assetOrigin: "NEW_ASSET", + imageUri: avatarValue, + description: `profilesets-${preset.name ?? "preset"}` + } + : avatarValue; + setPending("AVATAR", { avatar: avatarPayload }); } } if ("bannerDataUrl" in preset && preset.bannerDataUrl !== current.bannerDataUrl) { - setPending(`${profilePrefix}_SET_PENDING_BANNER`, { banner: preset.bannerDataUrl }); + setPending("BANNER", { banner: preset.bannerDataUrl }); } - if (!options.skipBio && "bio" in preset && preset.bio !== current.bio) { + if (!options.skipBio && preset?.bio !== current?.bio) { const bioValue = preset.bio === null ? "" : preset.bio; - setPending(`${profilePrefix}_SET_PENDING_BIO`, { bio: bioValue }); + setPending("BIO", { bio: bioValue }); } - if (!isGuild && !options.skipPronouns && "pronouns" in preset && preset.pronouns !== current.pronouns) { + if (!isGuild && !options.skipPronouns && preset?.pronouns !== current?.pronouns) { const pronounsValue = preset.pronouns === null ? "" : preset.pronouns; - setPending(`${profilePrefix}_SET_PENDING_PRONOUNS`, { pronouns: pronounsValue }); + setPending("PRONOUNS", { pronouns: pronounsValue }); } - if (!options.skipGlobalName && preset.globalName !== undefined && preset.globalName !== current.globalName) { + if (!options.skipGlobalName && preset?.globalName !== current?.globalName) { if (isGuild) { - setPending("GUILD_IDENTITY_SETTINGS_SET_PENDING_NICKNAME", { nickname: preset.globalName }); + setPending("NICKNAME", { nickname: preset.globalName }); } else { - setPending("USER_SETTINGS_ACCOUNT_SET_PENDING_GLOBAL_NAME", { globalName: preset.globalName }); + setPending("GLOBAL_NAME", { globalName: preset.globalName }); } } - if (preset.avatarDecoration !== undefined && !jsonEq(preset.avatarDecoration, current.avatarDecoration)) { - dispatchProfile(`${profilePrefix}_SET_PENDING_COLLECTIBLES_ITEM`, { + if (preset.avatarDecoration && ![pendingAvatarDecoration, equippedAvatarDecoration, current.avatarDecoration].some(d => avatarDecorationEq(preset.avatarDecoration, d))) { + setPending("COLLECTIBLES_ITEM", { item: { type: 0, value: preset.avatarDecoration } }); } - if (preset.profileEffect !== undefined && !jsonEq(preset.profileEffect, current.profileEffect)) { - dispatchProfile(`${profilePrefix}_SET_PENDING_COLLECTIBLES_ITEM`, { + if (preset.profileEffect && ![pendingProfileEffect, equippedProfileEffect, current.profileEffect].some(d => collectibleEqBySku(preset.profileEffect, d))) { + setPending("COLLECTIBLES_ITEM", { item: { type: 1, value: preset.profileEffect } }); } - if (preset.nameplate !== undefined && !jsonEq(preset.nameplate, current.nameplate)) { - dispatchProfile(`${profilePrefix}_SET_PENDING_COLLECTIBLES_ITEM`, { + if (preset.nameplate && ![pendingNameplate, equippedNameplate, current.nameplate].some(d => nameplateEq(preset.nameplate, d))) { + setPending("COLLECTIBLES_ITEM", { item: { type: 2, value: preset.nameplate } }); } - if (preset.displayNameStyles !== undefined) { - const presetDisplayNameStyles = normalizeDisplayNameStyles(preset.displayNameStyles as DisplayNameStylesLike | null | undefined); + if (preset.displayNameStyles) { + const presetDisplayNameStyles = normalizeDisplayNameStyles(preset.displayNameStyles); if (!jsonEq(presetDisplayNameStyles, current.displayNameStyles)) { - setPending(`${profilePrefix}_SET_PENDING_DISPLAY_NAME_STYLES`, { displayNameStyles: presetDisplayNameStyles }); + setPending("DISPLAY_NAME_STYLES", { displayNameStyles: presetDisplayNameStyles }); } } - if (preset.themeColors !== undefined && !jsonEq(preset.themeColors, current.themeColors)) { - setPending(`${profilePrefix}_SET_PENDING_THEME_COLORS`, { themeColors: preset.themeColors }); + if (preset.themeColors && !jsonEq(preset.themeColors, current.themeColors)) { + setPending("THEME_COLORS", { themeColors: preset.themeColors }); } - if (preset.primaryGuildId !== undefined && !isGuild && preset.primaryGuildId !== current.primaryGuildId) { + if (preset.primaryGuildId && !isGuild && preset.primaryGuildId !== current.primaryGuildId) { dispatch("USER_SETTINGS_SET_PENDING_PRIMARY_GUILD_ID", { primaryGuildId: preset.primaryGuildId }); } - if (preset.customStatus !== undefined && !isGuild && !customStatusEq(preset.customStatus, current.customStatus)) { - const customStatusValue = preset.customStatus ?? { - text: "", - expiresAtMs: "0", - emojiId: "0", - emojiName: "" - }; + if (preset.customStatus && !isGuild && !customStatusEq(preset.customStatus, current.customStatus)) { CustomStatusSettings.updateSetting({ - text: customStatusValue.text || "", - expiresAtMs: customStatusValue.expiresAtMs || "0", - emojiId: customStatusValue.emojiId || "0", - emojiName: customStatusValue.emojiName || "" + text: preset.customStatus?.text ?? "", + expiresAtMs: preset.customStatus?.expiresAtMs ?? "0", + emojiId: preset.customStatus?.emojiId ?? "0", + emojiName: preset.customStatus?.emojiName ?? "" }); } } catch (err) { throw err; } -} \ No newline at end of file +} diff --git a/src/equicordplugins/themeLibrary/index.tsx b/src/equicordplugins/themeLibrary/index.tsx index befdadca0..c634bb119 100644 --- a/src/equicordplugins/themeLibrary/index.tsx +++ b/src/equicordplugins/themeLibrary/index.tsx @@ -26,7 +26,7 @@ export default definePlugin({ start() { SettingsPlugin.customEntries.push({ - key: "theme_library", + key: "equicord_theme_library", title: "Theme Library", Component: require("./components/ThemeTab").default, Icon: ColorPaletteIcon diff --git a/src/equicordplugins/toneIndicators/index.tsx b/src/equicordplugins/toneIndicators/index.tsx index 655020748..6dc0ba1a0 100644 --- a/src/equicordplugins/toneIndicators/index.tsx +++ b/src/equicordplugins/toneIndicators/index.tsx @@ -24,11 +24,6 @@ const settings = definePluginSettings({ description: "Custom tone indicators (format: jk=Joking; srs=Serious)", default: "", }, - maxIndicators: { - type: OptionType.NUMBER, - description: "Maximum number of tone indicators to show per message", - default: 5, - }, }); function getCustomIndicators(): Record { @@ -91,15 +86,17 @@ function buildIndicatorRegex(): RegExp { function splitTextWithIndicators(text: string): ReactNode[] { const nodes: ReactNode[] = []; let lastIndex = 0; - let count = 0; const regex = buildIndicatorRegex(); const prefix = settings.store.prefix || "/"; let match: RegExpExecArray | null; - while ((match = regex.exec(text)) && count < settings.store.maxIndicators) { + while ((match = regex.exec(text))) { const indicator = match[1]; const desc = getIndicator(indicator); + const fullMatch = match[0]; + const leadingWhitespace = fullMatch.match(/^(\s*)/)?.[1] ?? ""; + const matchStart = match.index; const matchEnd = regex.lastIndex; @@ -108,6 +105,7 @@ function splitTextWithIndicators(text: string): ReactNode[] { } if (desc) { + if (leadingWhitespace) nodes.push(leadingWhitespace); nodes.push( , ); - count++; } lastIndex = matchEnd; diff --git a/src/equicordplugins/waitForSlot/index.tsx b/src/equicordplugins/waitForSlot/index.tsx index f9a345249..fbcfd636e 100644 --- a/src/equicordplugins/waitForSlot/index.tsx +++ b/src/equicordplugins/waitForSlot/index.tsx @@ -12,7 +12,7 @@ import { Devs, EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import type { Channel } from "@vencord/discord-types"; import { ChannelType } from "@vencord/discord-types/enums"; -import { ChannelActions, ChannelStore, Menu, VoiceStateStore } from "@webpack/common"; +import { ChannelActions, ChannelStore, Menu, PermissionsBits, PermissionStore, VoiceStateStore } from "@webpack/common"; let waitingChannelId: string | null = null; @@ -33,6 +33,7 @@ const settings = definePluginSettings({ const ChannelContext: NavContextMenuPatchCallback = (children, { channel }) => { if (channel?.type !== ChannelType.GUILD_VOICE || !channel.userLimit) return; + if (PermissionStore.can(PermissionsBits.MOVE_MEMBERS, channel)) return; if (Object.keys(VoiceStateStore.getVoiceStatesForChannel(channel.id)).length < channel.userLimit) return; const isWaiting = waitingChannelId === channel.id; @@ -48,6 +49,7 @@ const ChannelContext: NavContextMenuPatchCallback = (children, { channel }) => { function promptVoiceChannel(channel: Channel | null | undefined): boolean { if (!channel || channel.type !== ChannelType.GUILD_VOICE || !channel.userLimit) return false; + if (PermissionStore.can(PermissionsBits.MOVE_MEMBERS, channel)) return false; if (Object.keys(VoiceStateStore.getVoiceStatesForChannel(channel.id)).length < channel.userLimit) return false; if (waitingChannelId === channel.id) return true; diff --git a/src/plugins/_api/nicknameIcons.ts b/src/plugins/_api/nicknameIcons.ts index 48cc4b70d..47c7b8905 100644 --- a/src/plugins/_api/nicknameIcons.ts +++ b/src/plugins/_api/nicknameIcons.ts @@ -15,7 +15,7 @@ export default definePlugin({ { find: "#{intl::USER_PROFILE_PRONOUNS}", replacement: { - match: /(?<=isVerifiedBot\(\)}\).{0,60})children:(\i)/, + match: /(?<=shouldUnderlineOnHover:null.{0,150})children:(\i)/, replace: "children:[...Vencord.Api.NicknameIcons._renderIcons({userId:arguments[0].user?.id}),$1]" } } diff --git a/src/plugins/accountPanelServerProfile/index.tsx b/src/plugins/accountPanelServerProfile/index.tsx index af19dfa5d..b1304e3fa 100644 --- a/src/plugins/accountPanelServerProfile/index.tsx +++ b/src/plugins/accountPanelServerProfile/index.tsx @@ -75,7 +75,7 @@ export default definePlugin({ patches: [ { - find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));", + find: ".WIDGETS_RTC_UPSELL_COACHMARK)", group: true, replacement: [ { diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx index e33178043..16d6b9ee0 100644 --- a/src/plugins/betterSettings/index.tsx +++ b/src/plugins/betterSettings/index.tsx @@ -7,6 +7,7 @@ import { definePluginSettings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import { buildPluginMenuEntries, buildThemeMenuEntries } from "@equicordplugins/equicordToolbox/menu"; +import SettingsPlugin from "@plugins/_core/settings"; import { Devs } from "@utils/constants"; import { classNameFactory } from "@utils/css"; import { Logger } from "@utils/Logger"; @@ -17,7 +18,17 @@ import type { HTMLAttributes, ReactElement } from "react"; import fullHeightStyle from "./fullHeightContext.css?managed"; -type SettingsEntry = { section: string, label: string; }; +type SettingsEntry = { + ariaLabel?: string; + element?: any; + label?: string; + newIndicator?: any; + newIndicatorDismissibleContentTypes?: number[]; + predicate?: () => boolean; + searchableTitles?: string[]; + section: string, + tabPredicate?: () => boolean; +}; const cl = classNameFactory(""); const Classes = findCssClassesLazy("animating", "baseLayer", "bg", "layer", "layers"); @@ -127,7 +138,7 @@ export default definePlugin({ predicate: () => settings.store.disableFade }, { // Load menu TOC eagerly - find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));", + find: "handleOpenSettingsContextMenu=", replacement: { match: /(?=handleOpenSettingsContextMenu=.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)))/, replace: "_vencordBetterSettingsEagerLoad=(async ()=>$1)();" @@ -145,7 +156,7 @@ export default definePlugin({ }, { match: /case \i\.\i\.DEVELOPER_OPTIONS:return \i;/, - replace: "$&case 'EquicordPlugins':return $self.buildPluginMenuEntries(true);$&case 'EquicordThemes':return $self.buildThemeMenuEntries();" + replace: "$&case 'EquicordPlugins':return $self.buildPluginMenuEntries(true);case 'EquicordThemes':return $self.buildThemeMenuEntries();" } ] }, @@ -173,6 +184,8 @@ export default definePlugin({ const items = [] as TransformedSettingsEntry[]; for (const item of list) { + if (!item.label || item.predicate != null && !item.predicate()) continue; + if (item.section === "HEADER") { keyMap.set(item.label, item.label); items.push({ section: item.label, items: [] }); @@ -181,6 +194,28 @@ export default definePlugin({ } } + keyMap.set("Equicord", "Equicord"); + const equicordItems: SettingsEntry[] = [ + { section: "EquicordSettings", label: "Settings" }, + { section: "EquicordPlugins", label: "Plugins" }, + { section: "EquicordThemes", label: "Themes" }, + { section: "EquicordUpdater", label: "Updater" }, + { section: "EquicordChangelog", label: "Changelog" }, + { section: "EquicordCloud", label: "Cloud" }, + { section: "EquicordBackupAndRestore", label: "Backup & Restore" }, + { section: "EquicordPatchHelper", label: "Patch Helper" }, + ]; + + for (const [section, key] of SettingsPlugin.settingsSectionMap) { + const entry = SettingsPlugin.customEntries.find(e => key.endsWith(e.key)); + if (entry) equicordItems.push({ section, label: entry.title }); + } + + items.push({ + section: "Equicord", + items: equicordItems + }); + return items; }, diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index 8f8998675..a3f5ca1a4 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -87,7 +87,7 @@ export default definePlugin({ }, // Current user area, at bottom of channels/dm list { - find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));", + find: ".WIDGETS_RTC_UPSELL_COACHMARK),", replacement: [ // Use Decor avatar decoration hook { diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 3cc27e81b..85b9b3ddf 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -461,11 +461,11 @@ export default definePlugin({ }, }, { - find: '"channel_mention"});', + find: 'getConfig({location:"channel_mention"})', replacement: { // Show inside voice channel instead of trying to join them when clicking on a channel mention - match: /(?<=getChannel\(\i\);if\(null!=(\i))(?=.{0,150}?selectVoiceChannel)/, - replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})` + match: /(?<=getChannel\(\i\);if\(null!=(\i)).{0,200}?return void (?=\i\.default\.selectVoiceChannel)/, + replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&` } }, { diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index 41b3a7181..4cfce6339 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -380,7 +380,7 @@ function getMentionNameElement(props: mentionProps): JSX.Element | null { let colorStrings: colorStringsType = undefined; if (isPluginEnabled(ircColors.name)) { - const color = ircColors.calculateNameColorForMessageContext({ message: { author: { id: userId } }, channel }); + const color = ircColors.calculateNameColorForMessageContext({ message: { author: author }, author: author, channel: channel }); if (color) { colorString = color; diff --git a/src/plugins/typingTweaks/index.tsx b/src/plugins/typingTweaks/index.tsx index f4b833693..1b72581d3 100644 --- a/src/plugins/typingTweaks/index.tsx +++ b/src/plugins/typingTweaks/index.tsx @@ -122,7 +122,7 @@ export default definePlugin({ patches: [ { - find: "#{intl::Q8lUnE::raw}", + find: "#{intl::SEVERAL_USERS_TYPING_STRONG}", group: true, replacement: [ { diff --git a/src/plugins/volumeBooster/index.ts b/src/plugins/volumeBooster/index.ts index 81dcf6633..cc3cad194 100644 --- a/src/plugins/volumeBooster/index.ts +++ b/src/plugins/volumeBooster/index.ts @@ -61,7 +61,7 @@ export default definePlugin({ ].map(find => ({ find, replacement: { - match: /(?<=maxValue:)\i\.\i\?(\d+?):(\d+?)(?=,)/, + match: /(?<=maxValue:)\i\.\i\?(\i\.\i|\d+?):(\i\.\i|\d+?)(?=,)/, replace: (_, higherMaxVolume, minorMaxVolume) => `${higherMaxVolume}*$self.settings.store.multiplier` } })),