Merge branch 'dev'
This commit is contained in:
commit
07be581a29
23 changed files with 510 additions and 150 deletions
240
AGENTS.md
Normal file
240
AGENTS.md
Normal file
|
|
@ -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`
|
||||
|
||||
---
|
||||
1
CLAUDE.md
Symbolic link
1
CLAUDE.md
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
AGENTS.md
|
||||
|
|
@ -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. */
|
||||
|
|
|
|||
2
packages/discord-types/src/common/User.d.ts
vendored
2
packages/discord-types/src/common/User.d.ts
vendored
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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:\[/,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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,"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<ProfilePreset, "name" | "timestamp">;
|
||||
const [renaming, setRenaming] = React.useState<number>(-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();
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)'
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
|
||||
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<ProfilePreset, "name" | "timestamp">, value: any, section: PresetSection, guildId?: string) {
|
||||
export async function updatePresetField<K extends keyof Omit<ProfilePreset, "name" | "timestamp">>(
|
||||
index: number,
|
||||
field: K,
|
||||
value: Omit<ProfilePreset, "name" | "timestamp">[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();
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> & {
|
||||
selectedAvatarRaw?: ImageInput;
|
||||
presetAvatarRaw?: ImageInput;
|
||||
selectedAvatarProcessed?: ImageInput;
|
||||
presetAvatarProcessed?: ImageInput;
|
||||
pendingAvatar?: ImageInput;
|
||||
avatar?: ImageInput;
|
||||
pendingBanner?: ImageInput;
|
||||
|
|
@ -30,39 +34,23 @@ type PendingChanges = Record<string, unknown> & {
|
|||
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<string, unknown>) {
|
||||
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<string | null> {
|
|||
async function processImage(imageData: ImageInput, userId: string, type: "avatar" | "banner", guildId?: string, useGuildPath?: boolean): Promise<string | null> {
|
||||
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<Omit<ProfilePreset, "name" | "timestamp">> {
|
||||
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<string, unknown>) => {
|
||||
dispatch(type, isGuild ? { ...payload, guildId } : 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);
|
||||
}
|
||||
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<string, unknown>) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, string> {
|
||||
|
|
@ -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(
|
||||
<ToneIndicator
|
||||
key={`ti-${matchStart}`}
|
||||
|
|
@ -116,7 +114,6 @@ function splitTextWithIndicators(text: string): ReactNode[] {
|
|||
desc={desc}
|
||||
/>,
|
||||
);
|
||||
count++;
|
||||
}
|
||||
|
||||
lastIndex = matchEnd;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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]"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export default definePlugin({
|
|||
|
||||
patches: [
|
||||
{
|
||||
find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));",
|
||||
find: ".WIDGETS_RTC_UPSELL_COACHMARK)",
|
||||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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})&&`
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export default definePlugin({
|
|||
|
||||
patches: [
|
||||
{
|
||||
find: "#{intl::Q8lUnE::raw}",
|
||||
find: "#{intl::SEVERAL_USERS_TYPING_STRONG}",
|
||||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
}
|
||||
})),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue