diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 000000000..d4f9ddb09 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,33 @@ +name: autofix.ci + +on: + pull_request: + +permissions: + contents: read + +jobs: + autofix: + name: autofix + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v6 + + - name: Use Node.js 24 + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run lint fix + run: pnpm lint:fix + + - name: Run stylelint fix + run: pnpm lint-styles:fix + + - uses: autofix-ci/action@v1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df48d7a6f..d55249f75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,7 +73,7 @@ jobs: gh release upload latest --clobber dist/release/* - name: Upload Plugin JSONs to Equibored repo - if: github.repository == 'Equicord/Equicord' && !vars.ACT == 'false' + if: github.repository == 'Equicord/Equicord' && vars.ACT == 'false' run: | git config --global user.name "GitHub-Actions" git config --global user.email actions@github.com diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bd48b13b4..d066e4882 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -83,7 +83,7 @@ jobs: ".git/*" - name: Publish extension - if: steps.check.outputs.release == 'true' && !vars.ACT == 'false' + if: steps.check.outputs.release == 'true' && vars.ACT == 'false' run: pnpx @equicord/publish-browser-extension@latest --chrome-zip dist/extension-chrome.zip --firefox-zip dist/extension-firefox.zip --firefox-sources-zip diff --git a/.rules b/.rules index f5d7fa075..d29198c0f 100644 --- a/.rules +++ b/.rules @@ -1,213 +1,238 @@ -# Equicord Development Guidelines +# Equicord Development Rules -## Critical Rules +## Philosophy -- **NEVER** edit existing license headers. New files use 2026. -- Push back when the user idea is wrong or not correct. -- **NEVER** git checkout files without explicit permission. -- Always verify components/functions arent deprecated before using. -- **NEVER** use `any` for Discord objects. Import proper types from `@vencord/discord-types`. -- Fix with smallest possible change. No refactoring unless explicitly asked. -- Solve the problem at hand only. No patterns for patterns sake. -- **NEVER** overengineer. No premature abstractions, no helpers for one-time operations, no building for hypothetical futures. Three similar lines is better than a premature abstraction. -- Write modern, robust, professional code. Use current language features, proper error handling, and clean structure. -- Write natural human text in errors, descriptions, messages. No dashes or robotic formatting. Write like "Module not found" not "Module - not found". Keep it simple and conversational. - -## Code Rules - -- **DELETE** dead code, dont comment it. Git preserves history. -- No hardcoded values, unused imports, magic numbers. -- Import `classNameFactory` from `@utils/css` (NOT `@api/Styles` which is deprecated). **ALWAYS** use `cl()` for class names, never hardcode strings like `"vc-plugin-name-class"`. -- When combining multiple class names use `classes()` from `@utils/misc`, not template strings like `` `${a} ${b}` ``. -- Use utilities from `@utils/`, `@api/`, `@components/`. -- Use `Logger` from `@utils/Logger`, not `console.log`. -- Descriptions: capital first letter, end with period. -- Less code with same functionality is **ALWAYS** better. If it can be done in fewer lines without losing clarity, do it. -- Keep logic flat. Avoid deep nesting, prefer early returns, guard clauses, and simple control flow. -- **NEVER** add comments unless explicitly asked. -- Keep existing comments, they contain important context. -- Use `Map`/`Set`, `.some`/`.find`, no spread in loops, `Promise.all`. -- KISS over clever, flat over nested, explicit over implicit. +- Solve the problem at hand. No refactors unless asked. +- Smallest diff that fixes it. Three similar lines beats a premature helper. +- Less code with the same behavior wins. +- Never add comments unless asked. Keep existing comments, they carry context. +- Delete dead code. Don't comment it out. +- Natural human text in errors, descriptions, toasts. No dashes, no robotic phrasing. Descriptions start capitalized and end with a period. ## TypeScript -- **Prefer:** optional chaining (`?.`), nullish coalescing (`??`), `const`, arrow functions -- **Use:** destructuring, template literals, object shorthand, array methods -- **Style:** early returns, trust inference, inline single-use variables +- `strict: true`, `noImplicitAny: false`, `noUncheckedIndexedAccess: false`. Annotate params explicitly. Array index access is not auto `undefined`. +- Never `any`. Use `unknown` and narrow, or type webpack results via the `find*` generic. +- Never `as unknown as`. Write a type guard or use `satisfies`. +- Never `@ts-ignore`. Use `@ts-expect-error` with a one line reason only when unavoidable. +- Never `!` non null assertion. Narrow with `if` or early return. +- Never plain `enum`. Use `const enum` for internal closed sets. Use `as const` object plus literal union for cross boundary values. +- Never `namespace`. Modules only. +- Never relative `../../` when a `@alias` exists. +- Don't optional chain through values guaranteed to exist (`settings.store`, plugin proxies, find proxies). Optional chaining is for actually optional values. +- `import type` for type only imports. -## React +## Plugin Policy -- Wrap complex components with `ErrorBoundary.wrap(Component, { noop: true })`. -- Return `null` for conditional rendering, not `undefined`. -- **Forbidden:** `React.cloneElement`, `React.isValidElement`, `React.memo()`, `React.lazy`, `React.Children` -- Always return cleanup functions in `useEffect`. +What does NOT get merged: -## Forbidden +- Selfbot, automation of user actions, client state spoofing, anything abusing the API. +- Plugins requiring user supplied API keys or untrusted third party endpoints. +- Plugins targeting specific third party bots. Official Discord apps are fine. +- CSS only plugins that just hide or restyle UI. +- Functionality already trivial via built in plugins or Discord features. +- New dependencies without strong justification. -### Code +## Plugin Structure -- **No raw DOM manipulation.** No `document.querySelector`, no `MutationObserver`, no `element.style`. Always use webpack patches and React. -- Empty catch blocks. -- Hardcoded minified vars in patches (`e,t,n`). -- **NEVER** use `settings.use()` with arrays. Mutate then reassign. +- Folder per plugin. `src/plugins//index.tsx` or `src/equicordplugins//index.tsx`. Single file plugins still get a folder. +- Folder suffixes the build system filters on: `.desktop`, `.web`, `.dev`, `.discordDesktop`, `.vesktop`, `.equibop`. +- `export default definePlugin({ ... })`. Never named. +- Required fields: `name`, `description`, `authors`. Use `Devs` or `EquicordDevs`, never inline objects. +- List `dependencies` for every API plugin used. +- Prefer the folder suffix to lock environment. The `target?: "WEB" | "DESKTOP" | "BOTH"` field exists but is rarely used. Runtime `IS_DISCORD_DESKTOP`, `IS_WEB`, `IS_VESKTOP` (build time replaced) for partial gating inside a mixed plugin. +- `requiresRestart` defaults to true when `patches` exist. Set it only to override. -### Plugins +### Declarative over imperative -- **No selfbot or API abuse.** Nothing that automates user actions, spoofs client state, or abuses Discord's API in any way. -- No plugins that only do something trivially achievable with existing built-in plugins or Discord features. -- No CSS-only plugins that only hide or redesign UI elements. -- No plugins targeting specific third-party bots (official Discord apps are fine). -- No untrusted third-party APIs. No plugins requiring users to supply their own API keys. -- Do not introduce new dependencies unless strictly necessary and well justified. +Use these plugin object fields instead of calling `add*` or `remove*` manually. `PluginManager` handles lifecycle and auto enables the matching API plugin. -## Anti-Patterns +`commands`, `contextMenus`, `flux`, `onBeforeMessageSend`, `onBeforeMessageEdit`, `onMessageClick`, `chatBarButton`, `chatBarButtonWrapper`, `messagePopoverButton`, `renderMessageAccessory`, `renderMessageDecoration`, `renderMemberListDecorator`, `renderNicknameIcon`, `renderProfileCollection`, `renderProfileSection`, `headerBarButton`, `userAreaButton`, `userProfileBadge`, `managedStyle`, `toolboxActions`, `audioProcessor`. -- `value !== null && value !== undefined` ⟹ `value != null` -- `array && array.length > 0` ⟹ `array.length` (only when array is guaranteed to exist) -- `settings?.store?.value` ⟹ `settings.store.value` -- `value || defaultValue` ⟹ `value ?? defaultValue` +Wrapper style fields take `{ render | wrapper, priority }`, never a bare function. + +Imperative `add*` and `remove*` is only valid when registering multiple items of the same kind, registering conditionally, or registering after `start()`. Always pair with removal in `stop()`. Never mix the two for the same item. + +### Lifecycle + +- `start()` runs at `StartAt.WebpackReady` by default. +- `stop()` must reverse everything `start()` did. +- Never throw from `start` or `stop`. +- Plugin methods called via `$self` must be on the plugin object. Closures over module locals aren't callable. +- Never `Vencord.Plugins.plugins["Name"]`. Import the plugin directly and use `isPluginEnabled(name)` from `@api/PluginManager`. + +## Settings + +- `definePluginSettings({...})` from `@api/Settings`. Assign to `settings`. +- Every non SELECT setting needs `default`. SELECT puts `default: true` on one option. +- `restartNeeded: true` only when the value feeds a patch replacement or eager load decision. +- `hidden` and `disabled` can be functions for conditional UI. Never delete keys to "remove" them. Write a migration. +- `withPrivateSettings()` for internal state. Never widen the global `Settings` interface. +- Renames go at module top before `definePlugin`. Use `migratePluginSettings("New", "Old", ...)` and `migratePluginSetting(...)`. + +### Reading + +- React: `settings.use([...specificKeys])`. Hoist the keys array to module scope or memoize it. A fresh array literal resubscribes every render. +- Never `settings.use()` with no argument inside list rendered components. +- Hot paths: destructure `settings.store.x` once. Never re read in loops or render rows. +- `OptionType.COMPONENT` components must be module level. Use the `setValue` and `setError` props. Never write `settings.store.x` directly inside them. + +## Flux + +- Plugin lifetime subscriptions: `flux: { ACTION_TYPE(payload) { ... } }` on the plugin object. +- Component scoped or one shot: `FluxDispatcher.subscribe` inside `useEffect`, paired with `unsubscribe(sameRef)` in cleanup. +- Never declare both `flux` and manual `subscribe` for the same event in one plugin. ## Patching -### Core Principles +- One patch per concern. Minimum code touched. +- `find` is a string when possible. Anchor on stable tokens: intl keys (`#{intl::KEY}`), method names with trailing punctuation, action type literals, distinct CSS or path literals. +- Prefer intl key anchors over English copy. +- `match` is always a RegExp. Use `\i` for every minified identifier. Never letters or named patterns. +- Bound every gap with `.{0,N}?`. Never `.+?` or `.*?` unbounded. Cap at the smallest N that works. +- Use lookbehind and lookahead to inject without re consuming context. +- Capture only what you reuse. Convert reused only groups to `(?:...)` or move to lookarounds. +- `#{intl::KEY}` in match. The build hashes it. Never hardcode hashed forms like `.aA4Vce`. +- `/g` only when intentionally hitting multiple sites in one module. +- Replace: `$&` keeps the match, `$1` and `$2` for captures. Call into plugin code via `$self.fn(...)`. +- Never inject a statement where Discord expects an expression. +- `group: true` when multiple replacements in one patch must land together. +- Scope conditional patches with `predicate`, `fromBuild`, `toBuild`. Never branch inside `replace`. +- `all: true` for intl patches that may match across multiple bundles (English fallback plus user locale). +- Never `try/catch` to silence a failing replacement. Fix the match. +- Never depend on argument names or object key order. Use `arguments[i]`. +- Prefer multiple small patches with tight `find`s over one patch with a broad find and many replacements. -- Minimum code touched. Stability over cleverness. One patch per concern. -- Try multiple approaches, keep simplifying until you find the cleanest solution. -- Remove any regex character that doesnt change the match result. -- Not overengineered, not overlooked, just clean and future-proof. - -### Find Strings - -- Use `#{intl::KEY}` in find when possible, most stable anchor. -- If find matches multiple modules, add nearby stable string like `"),icon:"` or function name. -- `"#{intl::PIN_MESSAGE}),icon:"` is better than generic strings. - -### Match Patterns - -- Match only what you need to replace, let find do the targeting. -- Can use `#{intl::KEY}` in match too, gets canonicalized to hash. -- `/#{intl::KEY}\)/` beats `/label:\i\.pinned\?.{0,60}#{intl::KEY}\)/` -- `$&` keeps original, append/prepend to it. -- Use bounded gaps: `.{0,50}` not `.+?` or `.*?` -- Only use capture groups if reusing in replace. - -### NEVER Do - -- **Hardcoded minified vars:** `e,t,n,r,i,o,s,l,c,u,$_,xx,eD,eH,eW` — use `\i` instead -- **Minified chains:** `\i\.\i` alone — surround with stable strings -- **Unbounded gaps:** `.+?` or `.*?` — use `.{0,N}` -- **Generic patterns:** `/className:\i/` alone — add anchor -- **Raw intl hashes:** `.aA4Vce` — use `#{intl::KEY_NAME}` - -### Patch Examples - -**Clean replacement:** +### Examples ```js +// clean replacement find: "#{intl::PIN_MESSAGE}),icon:" match: /#{intl::PIN_MESSAGE}\)/ replace: "$self.getPinLabel(arguments[0]))" -``` -**Appending:** - -```js +// appending find: "#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}" match: /#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}.{0,100}(?=])/ replace: "$&,$self.renderTooltip(arguments[0].guild)" -``` -**Wrapping:** - -```js +// wrapping with lookbehind find: "#{intl::SEVERAL_USERS_TYPING_STRONG}" match: /(?<="aria-atomic":!0,children:)\i/ replace: "$self.renderTypingUsers({ children: $& })" ``` -## Plugin Interop +## Webpack access -- `isPluginEnabled(name)` from `@api/PluginManager` takes a string, checks required/isDependency/enabled. -- Import the plugin directly from its path to access `.name` and functions. -- Dont use `Vencord.Plugins.plugins` or `plugin.started`. Dont use `"as unknown as"` casting. +- Top level: lazy finders only. `findByPropsLazy`, `findByCodeLazy`, `findComponentByCodeLazy`, `findStoreLazy`, `findCssClassesLazy`, `getUserSettingLazy`, `proxyLazyWebpack`. +- Direct `find(...)` only inside `start()` or later runtime callbacks. +- Use `findComponentByCode` or `waitForComponent` for Discord components, not `findByProps`. Survives bundle reshuffles. +- `proxyLazy` for values needing same tick destructuring. `makeLazy` for plain deferred values. +- `@webpack/common` exports backed by `waitFor` (stores, `FluxDispatcher`, `Parser`, `Alerts`, hooks) are `undefined` until webpack is ready. Reference inside functions, not at module top. +- No `eagerPatches` or `wreq(id)` brute forcing. -```ts -import { isPluginEnabled } from "@api/PluginManager"; -import otherPlugin from "@equicordplugins/otherPlugin"; -if (!isPluginEnabled(otherPlugin.name)) return null; -otherPlugin.someFunction(); -``` +## React + +- Import hooks and `React` from `@webpack/common`. Never `import React from "react"`. Only `import type` from `"react"` (`ReactNode`, `ComponentType`, etc.). +- Forbidden: `React.FC`, `React.memo`, `React.lazy`, `React.Children`, `React.cloneElement`, `React.isValidElement`. Type props with a named `interface`. +- Conditional rendering returns `null`, never `undefined` or `false`. +- Wrap every component injected by a patch with `ErrorBoundary.wrap(C, { noop: true })`. Same for components exported as plugin surface. +- `LazyComponent(() => findByCode(...))` for webpack sourced components used before webpack is ready. +- `useStateFromStores([Store1, ...], () => derive(), deps?, areEqual?)`. List every store the selector reads. Pass `areEqual` when the selector returns objects or arrays. Selector must be pure and synchronous. + +## CSS / Classes + +- `classNameFactory` from `@utils/css`. Never from `@api/Styles`. That re export is deprecated, kept for Vesktop backcompat. +- Prefix is `vc--`. Usage: `cl("base", ["a", "b"], { active: cond })`. +- Combine classes with `classes(a, b)` from `@utils/misc`. Never template strings, never `+`. +- Discord mangled classes via `findCssClassesLazy("propA", ...)`. Never hardcode mangled suffixes. +- Static stylesheet: `import "./style.css"`. Runtime toggleable: `import name from "./style.css?managed"` plus `enableStyle`, `disableStyle`, `toggleStyle`, `setStyleClassNames` from `@api/Styles`. +- CSS variable selectors in managed CSS use `[--varName]`. The build rewrites these via the `setStyleClassNames` map. Never hardcode mangled selectors in CSS. +- Every selector in plugin CSS starts with `.vc--`. No bare element selectors, no `:root`, no `@import`. +- Prefer Discord CSS vars like `var(--background-primary)` over literal colors. +- Inline `style={{}}` only for dynamic per render values. +- No `!important` except to override a Discord rule unbeatable by specificity. + +## Native / Desktop + +- Desktop only plugin: folder suffix `.desktop`. Mixed target plugins with a `native.ts` do not use the suffix. +- `native.ts` next to `index.tsx`. Exports auto register as IPC handlers via `~pluginNatives`. +- Handler first arg is `IpcMainInvokeEvent`. Renderer side: + ```ts + const Native = VencordNative.pluginHelpers.MyPlugin as PluginNative; + ``` +- `pluginHelpers` key matches `definePlugin.name`. +- Renderer must never import `fs`, `path`, `net`, `https`, `child_process`, `electron`. Go through `native.ts` and validate input there. +- File pickers: `saveFile` and `chooseFile` from `@utils/web`. Cross platform, no IPC needed. +- Only `src/preload.ts` may `contextBridge.exposeInMainWorld`. + +## Never hardcode + +- CDN URLs (`cdn.discordapp.com/...`) → `IconUtils`. +- API paths (`/api/v9`, `/users/@me`) → `Constants.Endpoints` or `RestAPI`. +- Intl strings → `#{intl::KEY}`. +- Mangled CSS classes → `findCssClassesLazy`. +- Minified vars in patches (`e,t,n,r,i,o,s,...`) → `\i`. +- `console.log`, `warn`, `error` → `new Logger("name")` from `@utils/Logger`. +- Toast type strings → `Toasts.Type.*`. +- Class name concatenation → `classes(...)`. + +## Forbidden + +- Raw DOM. No `document.querySelector`, `MutationObserver`, `element.style`. Use webpack patches and React. +- Empty `catch` blocks. +- `settings.use()` with arrays mutated in place. Reassign with a new array. ## Reference -### Types +### Types from `@vencord/discord-types` +`Channel`, `Guild`, `GuildMember`, `User`, `Role`, `Message`, `Command`, `FluxEvents`, store types. -`Channel`, `Guild`, `GuildMember`, `User`, `Role`, `Message` from `@vencord/discord-types` +### `@api/*` +`Settings`, `Commands`, `DataStore`, `ContextMenu`, `Badges`, `Notices`, `Notifications`, `MessageEvents`, `MessageUpdater`, `MessageAccessories`, `MessageDecorations`, `MessagePopover`, `MemberListDecorators`, `ServerList`, `ChatButtons`, `HeaderBar`, `UserArea`, `NicknameIcons`, `ProfileCollections`, `ProfileSections`, `AudioPlayer`, `UserSettings`, `Styles`, `PluginManager`. -### Components +Deprecated: `@api/Themes`, and the `classNameFactory` re export from `@api/Styles`. -`Paragraph`, `BaseText`, `Flex`, `Button`, `ErrorCard` from `@components/` +### `@utils/*` +- `Logger`. `new Logger(name)`. +- `css`. `classNameFactory`, `classNameToSelector`. +- `misc`. `classes`, `sleep`, `isObject`, `isObjectEmpty`, `pluralise`, `parseUrl`, `identity`, `interpolateIfDefined`, `tryOrElse`, `isPluginDev`, `removeFromArray`. +- `text`. `formatDuration`, `formatDurationMs`, `humanFriendlyJoin`, `makeCodeblock`, `toInlineCode`, `escapeRegExp`, `stripIndent`, `ZWSP`. +- `react`. `useAwaiter`, `useForceUpdater`, `useIntersection`, `useTimer`, `useFixedTimer`, `useCleanupEffect`, `NoopComponent`. +- `lazy`. `makeLazy`, `proxyLazy`. +- `lazyReact`. `LazyComponent`. +- `modal`. `openModal`, `openModalLazy`, `closeModal`, `closeAllModals`, `openMediaModal`, `ModalRoot`, `ModalHeader`, `ModalContent`, `ModalFooter`, `ModalCloseButton`, `ModalSize`. +- `discord`. `getCurrentChannel`, `getCurrentGuild`, `getIntlMessage`, `openUserProfile`, `openPrivateChannel`, `sendMessage`, `insertTextIntoChatInputBox`, `copyWithToast`, `getUniqueUsername`, `fetchUserProfile`, `openImageModal`, `openInviteModal`, `getTheme`, `Theme`, `hasGuildFeature`. +- `clipboard`. `copyToClipboard`, `readClipboard`. +- `margins`. `Margins`. +- `guards`. `isTruthy`, `isNonNullish`. +- `web`. `saveFile`, `chooseFile`. +- `constants`. `Devs`, `EquicordDevs`, `IS_WINDOWS`, `IS_MAC`, `IS_LINUX`, `IS_MOBILE`. +- `types`. `OptionType`, `StartAt`, `ReporterTestable`, `definePlugin`. -### Settings +### `@components/*` +`Paragraph`, `BaseText`, `Flex`, `Button`, `ErrorCard`, `ErrorBoundary`. -Use `definePluginSettings` from `@api/Settings`, not inline settings object. +### `@webpack/common` -### Utilities +Stores. `UserStore`, `GuildStore`, `GuildRoleStore`, `GuildMemberStore`, `GuildChannelStore`, `ChannelStore`, `SelectedChannelStore`, `SelectedGuildStore`, `MessageStore`, `PresenceStore`, `RelationshipStore`, `PermissionStore`, `ReadStateStore`, `EmojiStore`, `StickersStore`, `ThemeStore`, `VoiceStateStore`, `TypingStore`, `DraftStore`, `WindowStore`, `AccessibilityStore`, `PendingReplyStore`, `UserProfileStore`, `StreamerModeStore`, `SpotifyStore`, `NotificationSettingsStore`, `LocaleStore`, `ExperimentStore`, `QuestStore`. Full list in `src/webpack/common/stores.ts`. -- **@utils/clipboard** — `copyToClipboard` -- **@utils/discord** — `insertTextIntoChatInputBox`, `getCurrentChannel`, `getCurrentGuild`, `getIntlMessage`, `openUserProfile`, `openPrivateChannel`, `sendMessage`, `copyWithToast`, `getUniqueUsername`, `fetchUserProfile` -- **@utils/css** — `classNameFactory`, `classNameToSelector` -- **@utils/misc** — `classes`, `sleep`, `isObject`, `isObjectEmpty`, `pluralise`, `parseUrl`, `identity` -- **@utils/text** — `formatDuration`, `formatDurationMs`, `humanFriendlyJoin`, `makeCodeblock`, `toInlineCode`, `escapeRegExp` -- **@utils/modal** — `openModal`, `closeModal`, `ModalRoot`, `ModalHeader`, `ModalContent`, `ModalCloseButton` -- **@utils/margins** — `Margins` (`Margins.top8`, `Margins.bottom16`, etc.) -- **@utils/guards** — `isTruthy`, `isNonNullish` -- **@utils/web** — `saveFile`, `chooseFile` +React and hooks. `React`, `useState`, `useEffect`, `useLayoutEffect`, `useMemo`, `useRef`, `useReducer`, `useCallback`, `useStateFromStores`. -### Webpack Common +Components. `Tooltip`, `TextInput`, `TextArea`, `Select`, `SearchableSelect`, `Slider`, `Checkbox`, `Avatar`, `Menu`, `Popout`, `Dialog`, `TabBar`, `Clickable`, `MaskedLink`, `Timestamp`, `FocusLock`, `UserSummaryItem`, `ColorPicker`, `OAuth2AuthorizeModal`, `ScrollerThin`, `ScrollerAuto`, `ScrollerNone`, `ListScrollerThin`, `ListScrollerAuto`. -#### IconUtils +Utils. `FluxDispatcher`, `RestAPI`, `Constants`, `SnowflakeUtils`, `Parser`, `PermissionsBits`, `Alerts`, `Toasts`, `showToast`, `moment`, `lodash`, `i18n`, `IconUtils`, `ColorUtils`, `ImageUtils`, `DateUtils`, `UsernameUtils`, `DisplayProfileUtils`, `URLUtils`, `Humanize`, `EmojiUtils`, `UserUtils`, `UploadHandler`, `UploadManager`, `ComponentDispatch`, `ExpressionPickerStore`, `ReadStateUtils`, `PopoutActions`, `ApplicationAssetUtils`. -From `@webpack/common`. **NEVER** hardcode `cdn.discordapp.com` URLs. +Routers. `NavigationRouter`, `ChannelRouter`, `SettingsRouter`. -- `IconUtils.getUserAvatarURL(user, canAnimate?, size?)` -- `IconUtils.getDefaultAvatarURL(id)` -- `IconUtils.getUserBannerURL({ id, banner, canAnimate?, size })` -- `IconUtils.getGuildIconURL({ id, icon, size?, canAnimate? })` -- `IconUtils.getGuildBannerURL(guild, canAnimate?)` -- `IconUtils.getChannelIconURL({ id, icon })` -- `IconUtils.getEmojiURL({ id, animated, size })` -- `IconUtils.getApplicationIconURL(data)` -- `IconUtils.getGameAssetURL(data)` +Action creators. `MessageActions`, `ChannelActionCreators`, `UserProfileActions`, `InviteActions`, `GuildActions`, `VoiceActions`, `DraftActions`, `PinActions`, `MessageCache`. -#### Stores +Menu. `Menu`, `ContextMenuApi` (`openContextMenu`, `closeContextMenu`, `openContextMenuLazy`). -`UserStore`, `GuildStore`, `ChannelStore`, `GuildMemberStore`, `SelectedChannelStore`, `SelectedGuildStore`, `PresenceStore`, `RelationshipStore`, `MessageStore`, `EmojiStore`, `ThemeStore`, `PermissionStore`, `VoiceStateStore` from `@webpack/common` +### Lazy webpack imports +From `@webpack`. `findByPropsLazy`, `findByCodeLazy`, `findComponentLazy`, `findComponentByCodeLazy`, `findExportedComponentLazy`, `findStoreLazy`, `findCssClassesLazy`, `findLazy`, `proxyLazyWebpack`, `LazyComponentWebpack`, `mapMangledModuleLazy`, `extractAndLoadChunksLazy`, `waitFor`. -#### Actions +`waitForStore` and `waitForComponent` live in `@webpack/common/internal.tsx`. They're used by `@webpack/common` itself, not directly by plugins. -`RestAPI`, `FluxDispatcher`, `MessageActions`, `NavigationRouter`, `ChannelRouter`, `ChannelActionCreators`, `SettingsRouter` from `@webpack/common` - -#### Utils - -`Constants` (`Constants.Endpoints`), `SnowflakeUtils`, `Parser`, `PermissionsBits`, `moment`, `lodash`, `ColorUtils`, `ImageUtils`, `DateUtils`, `UsernameUtils`, `DisplayProfileUtils`, `URLUtils`, `Humanize`, `EmojiUtils` from `@webpack/common` - -#### Components - -`Tooltip`, `TextInput`, `TextArea`, `Select`, `Slider`, `Avatar`, `Menu`, `Popout`, `ScrollerThin`, `Timestamp` from `@webpack/common` - -#### Toasts - -`Toasts`, `showToast` from `@webpack/common` - -### Imports - -- **Lazy** from `@webpack` — `findByPropsLazy`, `findStoreLazy`, `findComponentByCodeLazy`, `findExportedComponentLazy` -- **Common** from `@webpack/common` — `useState`, `useEffect`, `useCallback`, `useStateFromStores` - -### Never Hardcode - -- `"cdn.discordapp.com/avatars/"` etc. ⟹ `IconUtils` -- `"/api/v9"`, `"/users/@me"` ⟹ `Constants.Endpoints` or `RestAPI` -- `console.log/warn/error` ⟹ `Logger` from `@utils/Logger` -- `` `${a} ${b}` `` for class names ⟹ `classes(a, b)` from `@utils/misc` +### `IconUtils` methods +`getUserAvatarURL(user, canAnimate?, size?, format?)`, `getUserBannerURL({ id, banner, canAnimate?, size })`, `getGuildIconURL({ id, icon, size?, canAnimate? })`, `getGuildBannerURL(guild, canAnimate?)`, `getGuildSplashURL`, `getGuildDiscoverySplashURL`, `getGuildHomeHeaderURL`, `getChannelIconURL({ id, icon })`, `getResourceChannelIconURL`, `getEmojiURL({ id, animated, size })`, `getApplicationIconURL`, `getGameAssetURL`, `getVideoFilterAssetURL`, `getAvatarDecorationURL`, `getGuildMemberAvatarURL`, `getGuildMemberAvatarURLSimple`, `getGuildMemberBannerURL`, `getDefaultAvatarURL(id)`, `hasAnimatedGuildIcon`, `isAnimatedIconHash`. diff --git a/README.md b/README.md index 9d4992b1a..ddb34ecf9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ Windows MacOS -- [GUI](https://github.com/Equicord/Equilotl/releases/latest/download/Equilotl.MacOS.zip) +- [X64 GUI](https://github.com/Equicord/Equilotl/releases/latest/download/Equilotl-darwin-x64.zip) +- [ARM64 GUI](https://github.com/Equicord/Equilotl/releases/latest/download/Equilotl-darwin-arm64.zip) Linux diff --git a/browser/VencordNativeStub.ts b/browser/VencordNativeStub.ts index 46cba9d0c..746d41e48 100644 --- a/browser/VencordNativeStub.ts +++ b/browser/VencordNativeStub.ts @@ -55,6 +55,7 @@ window.VencordNative = { native: { getVersions: () => ({}), + supportsWindowsMaterial: () => false, openExternal: async (url) => void open(url, "_blank"), getRendererCss: async () => { if (IS_USERSCRIPT) diff --git a/package.json b/package.json index 472d56a5b..b26f1c271 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "equicord", "private": "true", - "version": "1.14.12.0", + "version": "1.14.13.0", "description": "The other cutest Discord client mod", "homepage": "https://github.com/Equicord/Equicord#readme", "bugs": { @@ -33,8 +33,11 @@ "repair": "node scripts/runInstaller.mjs -- --repair", "lint": "eslint", "lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins", + "lint-styles:fix": "pnpm lint-styles --fix", "lint:fix": "pnpm lint --fix", - "test": "pnpm buildStandalone && pnpm testTsc && pnpm lint:fix && pnpm lint-styles && pnpm generatePluginJson", + "lint:intl": "node scripts/lintIntl.mjs", + "lint:patches": "node scripts/lintPatches.mjs", + "test": "pnpm buildStandalone && pnpm testTsc && pnpm lint:fix && pnpm lint-styles && pnpm lint:intl && pnpm lint:patches && pnpm generatePluginJson", "testWeb": "pnpm lint:fix && pnpm buildWeb && pnpm testTsc", "testTsc": "tsc --noEmit", "depCheck": "node scripts/depCheck.mjs" @@ -98,31 +101,7 @@ "typescript-transform-paths": "^3.5.6", "zip-local": "^0.3.5" }, - "packageManager": "pnpm@10.33.0", - "pnpm": { - "patchedDependencies": { - "@types/less@3.0.8": "patches/@types__less@3.0.8.patch" - }, - "peerDependencyRules": { - "ignoreMissing": [ - "eslint-plugin-import", - "eslint" - ] - }, - "allowedDeprecatedVersions": { - "source-map-resolve": "*", - "resolve-url": "*", - "source-map-url": "*", - "urix": "*", - "q": "*" - }, - "onlyBuiltDependencies": [ - "esbuild" - ], - "overrides": { - "@eslint/plugin-kit@<0.3.4": ">=0.3.4" - } - }, + "packageManager": "pnpm@11.0.9", "engines": { "node": ">=18" } diff --git a/packages/discord-types/src/components.d.ts b/packages/discord-types/src/components.d.ts index c7d01e481..a207ba755 100644 --- a/packages/discord-types/src/components.d.ts +++ b/packages/discord-types/src/components.d.ts @@ -192,6 +192,7 @@ export type TextInput = ComponentType, "onChange"> & { onChange(v: string): void; inputRef?: Ref; + autosize?: boolean; }>; export interface SelectOption { diff --git a/packages/discord-types/src/components/Modal.d.ts b/packages/discord-types/src/components/Modal.d.ts new file mode 100644 index 000000000..e8586b647 --- /dev/null +++ b/packages/discord-types/src/components/Modal.d.ts @@ -0,0 +1,136 @@ +import { ComponentType, ReactNode, Ref, RefObject } from "react"; +import { LiteralUnion } from "type-fest"; + +export type ModalActionVariant = LiteralUnion<"primary" | "secondary" | "critical-primary", string>; +export type ModalSize = LiteralUnion<"sm" | "md" | "lg" | "xl" | "xxl", string>; + +export interface ModalAction { + text: string; + variant: ModalActionVariant; + onClick(): void; + loading?: boolean; + disabled?: boolean; +} + +/** + * Modal with all options: https://files.catbox.moe/c8qxt0.png + */ +export interface ModalProps extends RenderModalProps { + size?: ModalSize; + role?: "alertdialog" | "dialog"; + + title: ReactNode; + /** Optional subtitle, will render below title (duh) */ + subtitle?: ReactNode; + + /** Optional Modal content */ + children?: ReactNode; + input?: ReactNode; + preview?: ReactNode; + + listProps?: any; + onScroll?(): void; + scrollerRef?: Ref; + + /** Action buttons at the bottom of the Modal */ + actions?: ModalAction[]; + /** Custom component to show before actions, useful for e.g. "Don't show again" checkbox */ + actionBarInput?: ReactNode; + actionBarInputLayout?: "default" | "chat-input"; + + /** Shows a Notice (card) between (sub-)title and content (children) */ + notice?: { + message: string; + type: LiteralUnion<"critical", string>; + }; +} + +export interface RenderModalProps { + transitionState: number; + onClose(): void; +} + +/** Wrapper around Modal */ +export interface ConfirmModalProps extends ModalProps { + /** Variant for the confirm button, defaults to critical */ + variant?: ModalActionVariant; + confirmText: ModalAction["text"]; + /** Defaults to "Cancel" */ + cancelText?: ModalAction["text"]; + /** + * If onConfirm runs without error, the modal will close. + * To indicate an error, throw an Error. This will show the user a notice inside the Modal and keep it open. + * This notice will either be a generic message if you just throw. If you call setError, it will show that message. + * Even if you call setError, you still have to throw to keep the Modal open + */ + onConfirm?(setError: (error: string) => void): void; + /** Optional callback that runs when the user cancels the action. Whether provided or not, the Modal will close when user clicks Cancel. */ + onCancel?(): void; + onCloseCallback?(): void; + /** Checkbox that shows before the action buttons */ + checkboxProps?: { + /** Defaults to "Don't show again" */ + label?: string; + checked: boolean; + onChange(checked: boolean): void; + }; + + /** Custom Modal content */ + children?: ReactNode; +} + +export type Modal = ComponentType; +export type ConfirmModal = ComponentType; + +export type RenderModal = (props: RenderModalProps) => ReactNode; + +export interface ModalOptions { + modalKey?: string; + onCloseRequest?(): void; + onCloseCallback?(): void; +} + +export interface ModalAPI { + /** + * Wait for the render promise to resolve, then open a modal with it. + * This is usually preferable to render().then(openModal), because it shows a Loading modal + */ + openModalLazy: (renderModal: () => Promise, options?: ModalOptions & { contextKey?: string; }) => Promise; + /** + * Open a Modal with the given render function. + */ + openModal: (renderModal: RenderModal, options?: ModalOptions, contextKey?: string) => string; + /** + * Close a modal by its key + */ + closeModal: (modalKey: string, contextKey?: string) => void; + /** + * Close all open modals + */ + closeAllModals: () => void; +} + +export interface MediaModalItem { + url: string; + type: "IMAGE" | "VIDEO" | "CLIP"; + original?: string; + alt?: string; + width?: number; + height?: number; + animated?: boolean; + maxWidth?: number; + maxHeight?: number; +} + +export interface MediaModalProps { + location?: string; + contextKey?: string; + onCloseCallback?: () => void; + className?: string; + items: MediaModalItem[]; + startingIndex?: number; + onIndexChange?: (...args: any[]) => void; + fit?: string; + shouldRedactExplicitContent?: boolean; + shouldHideMediaOptions?: boolean; +} diff --git a/packages/discord-types/src/index.d.ts b/packages/discord-types/src/index.d.ts index 23ab505d8..83bbba26e 100644 --- a/packages/discord-types/src/index.d.ts +++ b/packages/discord-types/src/index.d.ts @@ -1,5 +1,7 @@ export * from "./common"; +// TODO: Splitting up components into files in the folder would be good export * from "./components"; +export * from "./components/Modal"; export * from "./flux"; export * from "./fluxEvents"; export * from "./menu"; diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9bec5ba55..0e9faf0f1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,23 @@ packages: - packages/* + +allowBuilds: + esbuild: true + +overrides: + "@eslint/plugin-kit@<0.3.4": ">=0.3.4" + +patchedDependencies: + "@types/less@3.0.8": patches/@types__less@3.0.8.patch + +peerDependencyRules: + ignoreMissing: + - eslint-plugin-import + - eslint + +allowedDeprecatedVersions: + source-map-resolve: "*" + resolve-url: "*" + source-map-url: "*" + urix: "*" + q: "*" diff --git a/scripts/lintIntl.mjs b/scripts/lintIntl.mjs new file mode 100644 index 000000000..c85812aa9 --- /dev/null +++ b/scripts/lintIntl.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { join, sep } from "node:path"; + +const ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"); +const VALID_MODIFIERS = new Set(["raw", "hash"]); +const MARKER_RE = /#\{intl::([\w$+/]*)(?:::(\w+))?\}/g; +const HASH_RE = /["'`]\s*\.([A-Za-z][A-Za-z0-9+/]{5})\b/g; + +const tracked = execFileSync("git", ["ls-files", "src"], { cwd: ROOT, encoding: "utf8" }) + .split("\n") + .filter(p => /\.(ts|tsx|js|jsx|mjs)$/.test(p)) + .map(p => p.replace(/\//g, sep)); + +let errors = 0; +let warnings = 0; + +for (const rel of tracked) { + const text = readFileSync(join(ROOT, rel), "utf8"); + if (!text.includes("#{intl::") && !text.includes("find:") && !text.includes("match:")) continue; + + const lines = text.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const at = `${rel.replace(/\\/g, "/")}:${i + 1}`; + + if (/#\{intl::[^}]*$/.test(line)) { + console.error(`${at}: ERROR unclosed #{intl:: marker`); + errors++; + } + for (const [full, key, modifier] of line.matchAll(MARKER_RE)) { + if (!key) { + console.error(`${at}: ERROR empty intl key: ${full}`); + errors++; + } + if (modifier && !VALID_MODIFIERS.has(modifier)) { + console.error(`${at}: ERROR unknown intl modifier "${modifier}": ${full}`); + errors++; + } + } + if (/\b(find|match)\s*:/.test(line) && !line.includes("#{intl::")) { + for (const [, hash] of line.matchAll(HASH_RE)) { + if (!/[0-9+/]/.test(hash)) continue; + console.warn(`${at}: WARN baked intl hash ".${hash}", prefer #{intl::KEY}`); + warnings++; + } + } + } +} + +console.log(`\nintl lint: ${errors} error(s), ${warnings} warning(s)`); +process.exit(errors > 0 ? 1 : 0); diff --git a/scripts/lintPatches.mjs b/scripts/lintPatches.mjs new file mode 100644 index 000000000..33a08483c --- /dev/null +++ b/scripts/lintPatches.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { join, sep } from "node:path"; + +const ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"); +const VERBOSE = process.env.LINT_PATCHES_VERBOSE === "1" || process.argv.includes("--verbose"); + +const tracked = execFileSync("git", ["ls-files", "src"], { cwd: ROOT, encoding: "utf8" }) + .split("\n") + .filter(p => /^src\/(plugins|equicordplugins)\/.*\.(ts|tsx)$/.test(p)) + .map(p => p.replace(/\//g, sep)); + +let errors = 0; +let warnings = 0; + +function fail(at, rule, msg) { + console.error(`${at}: ERROR ${rule} ${msg}`); + errors++; +} +function warn(at, rule, msg) { + if (VERBOSE) console.warn(`${at}: WARN ${rule} ${msg}`); + warnings++; +} + +for (const rel of tracked) { + const text = readFileSync(join(ROOT, rel), "utf8"); + if (!text.includes("match:") && !text.includes("find:")) continue; + + const lines = text.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const at = `${rel.replace(/\\/g, "/")}:${i + 1}`; + + const matchM = line.match(/\bmatch\s*:\s*\/((?:\\.|[^/\\\n])+)\/[gimsuy]*/); + if (matchM) { + const src = matchM[1]; + const unbounded = src.match(/\.[+*]\??/); + if (unbounded) warn(at, "P002", `unbounded ${unbounded[0]} in match`); + const minified = src.match(/\b[a-z]\.[a-z]\b/); + if (minified) fail(at, "P001", `hardcoded minified var "${minified[0]}", use \\i.\\i`); + } + + const findM = line.match(/\bfind\s*:\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\/(?:\\.|[^/\\\n])+\/[gimsuy]*)/); + if (findM) { + const window = lines.slice(i, i + 12).join("\n"); + if (/\b(replacement|match)\s*:/.test(window)) { + const v = findM[1]; + if (v === '""' || v === "''" || v === "``") { + fail(at, "P003", "find is empty"); + } else if (v.startsWith("/")) { + const src = v.slice(1, v.lastIndexOf("/")); + if (/\\i/.test(src) && !/[A-Za-z]{3,}/.test(src) && !src.includes("#{intl::")) { + warn(at, "P004", "find regex has no string anchor"); + } + } + } + } + + const replaceM = line.match(/\breplace\s*:\s*(["'`])((?:\\.|(?!\1).)*)\1/); + if (replaceM && /\btry\s*\{[\s\S]*?\}\s*catch\b/.test(replaceM[2])) { + warn(at, "P005", "replace uses try/catch, fix the match instead"); + } + } +} + +const tail = warnings && !VERBOSE ? " (run with --verbose to list)" : ""; +console.log(`\npatch lint: ${errors} error(s), ${warnings} warning(s)${tail}`); +process.exit(errors > 0 ? 1 : 0); diff --git a/scripts/runInstaller.mjs b/scripts/runInstaller.mjs index 94c03dd4a..0c124b170 100644 --- a/scripts/runInstaller.mjs +++ b/scripts/runInstaller.mjs @@ -38,7 +38,14 @@ function getFilename() { case "win32": return "EquilotlCli.exe"; case "darwin": - return "Equilotl.MacOS.zip"; + switch (process.arch) { + case "x64": + return "Equilotl-darwin-x64.zip"; + case "arm64": + return "Equilotl-darwin-arm64.zip"; + default: + throw new Error("Unsupported macOS architecture: " + process.arch); + } case "linux": return "EquilotlCli-linux"; default: diff --git a/src/Vencord.ts b/src/Vencord.ts index 1ce426d0f..189f67e7d 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -18,6 +18,7 @@ // DO NOT REMOVE UNLESS YOU WISH TO FACE THE WRATH OF THE CIRCULAR DEPENDENCY DEMON!!!!!!! import "~plugins"; +import "./fixWeirdAppRegionBug.css"; export * as Api from "./api"; export * as Plugins from "./api/PluginManager"; diff --git a/src/VencordNative.ts b/src/VencordNative.ts index d13ce4dd7..59969777f 100644 --- a/src/VencordNative.ts +++ b/src/VencordNative.ts @@ -77,6 +77,7 @@ export default { native: { getVersions: () => process.versions as Partial, + supportsWindowsMaterial: () => sendSync(IpcEvents.SUPPORTS_WINDOWS_MATERIAL), openExternal: (url: string) => invoke(IpcEvents.OPEN_EXTERNAL, url), getRendererCss: () => invoke(IpcEvents.GET_RENDERER_CSS), onRendererCssUpdate: (cb: (newCss: string) => void) => { diff --git a/src/api/ChatButtons.tsx b/src/api/ChatButtons.tsx index a04725386..ae1ea261c 100644 --- a/src/api/ChatButtons.tsx +++ b/src/api/ChatButtons.tsx @@ -138,7 +138,7 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => { return ( {({ onMouseEnter, onMouseLeave }) => ( -
+
{ ); }); -// Wrapper stuff is down here to avoid conflicts with above export type ChatBarButtonWrapper = (buttons: ReactNode) => ReactNode; -/** - * Registry for plugins that need to wrap the entire chat bar button container - */ -export const ChatBarButtonWrappers = new Map(); +export interface ChatBarButtonWrapperData { + wrapper: ChatBarButtonWrapper; + priority: number; +} -export const addChatBarButtonWrapper = (id: string, wrapper: ChatBarButtonWrapper) => ChatBarButtonWrappers.set(id, wrapper); +/** + * Registry for plugins that need to wrap the entire chat bar button container. + * Wrappers are applied in ascending priority order (lower number = outermost wrapper). + */ +export const ChatBarButtonWrappers = new Map(); + +export const addChatBarButtonWrapper = (id: string, wrapper: ChatBarButtonWrapper, priority: number = 0) => ChatBarButtonWrappers.set(id, { wrapper, priority }); export const removeChatBarButtonWrapper = (id: string) => ChatBarButtonWrappers.delete(id); export function _wrapButtons(buttons: ReactNode) { + const sorted = [...ChatBarButtonWrappers.values()] + .sort((a, b) => a.priority - b.priority); + let wrapped = buttons; - for (const wrapper of ChatBarButtonWrappers.values()) { + for (const { wrapper } of sorted) { wrapped = wrapper(wrapped); } return wrapped; diff --git a/src/api/Notifications/notificationLog.tsx b/src/api/Notifications/notificationLog.tsx index 633ea07be..f0063863c 100644 --- a/src/api/Notifications/notificationLog.tsx +++ b/src/api/Notifications/notificationLog.tsx @@ -18,15 +18,13 @@ import * as DataStore from "@api/DataStore"; import { Settings } from "@api/Settings"; -import { BaseText } from "@components/BaseText"; -import { Flex } from "@components/Flex"; import { Paragraph } from "@components/Paragraph"; import { openNotificationSettingsModal } from "@components/settings/tabs/vencord/NotificationSettings"; import { classNameFactory } from "@utils/css"; -import { closeModal, ModalCloseButton, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; import { t } from "@utils/translation"; -import { Alerts, Button, ListScrollerThin, React, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; +import { RenderModalProps } from "@vencord/discord-types"; +import { ConfirmModal, ListScrollerThin, Modal, openModal, React, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; import { nanoid } from "nanoid"; import type { DispatchWithoutAction } from "react"; @@ -133,7 +131,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) { export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) { if (!log.length && !pending) return ( -
+
{t("api.notifications.noNotifications")} @@ -153,56 +151,47 @@ export function NotificationLog({ log, pending }: { log: PersistentNotificationD ); } -function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) { +function LogModal(props: RenderModalProps) { const [log, pending] = useLogs(); return ( - - - {t("api.notifications.notificationLog")} - - - -
- -
- - - - - - - - -
+ }} + /> + ); + } + } + ]} + > + + ); } export function openNotificationLogModal() { - const key = openModal(modalProps => ( - closeModal(key)} - /> - )); + openModal(props => ); } diff --git a/src/api/Notifications/styles.css b/src/api/Notifications/styles.css index e15e01a7d..1d3085e98 100644 --- a/src/api/Notifications/styles.css +++ b/src/api/Notifications/styles.css @@ -89,12 +89,6 @@ margin-bottom: 40px; } -.vc-notification-log-container { - padding: 1em; - max-height: min(750px, 75vh); - width: 100%; -} - .vc-notification-log-wrapper { height: 120px; width: 100%; diff --git a/src/api/PluginManager.ts b/src/api/PluginManager.ts index 76a14c988..910c3e0bc 100644 --- a/src/api/PluginManager.ts +++ b/src/api/PluginManager.ts @@ -288,9 +288,9 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: } if (audioProcessor) addAudioProcessor(name, audioProcessor); if (userAreaButton) addUserAreaButton(name, userAreaButton.render, userAreaButton.priority); - if (renderProfileCollection) addProfileCollection(name, renderProfileCollection); - if (chatBarButtonWrapper) addChatBarButtonWrapper(name, chatBarButtonWrapper); - if (renderProfileSection) addProfileSection(name, renderProfileSection); + if (renderProfileCollection) addProfileCollection(name, renderProfileCollection.render, renderProfileCollection.priority); + if (chatBarButtonWrapper) addChatBarButtonWrapper(name, chatBarButtonWrapper.wrapper, chatBarButtonWrapper.priority); + if (renderProfileSection) addProfileSection(name, renderProfileSection.render, renderProfileSection.priority); return true; }, p => `startPlugin ${p.name}`); @@ -382,7 +382,7 @@ export const initPluginManager = onlyOnce(function init() { "onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick", "renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration", // Custom - "renderNicknameIcon", "renderProfileCollection" + "renderNicknameIcon" ]; const neededApiPlugins = new Set(); @@ -429,7 +429,7 @@ export const initPluginManager = onlyOnce(function init() { if (p.renderProfileSection) neededApiPlugins.add("ProfileSectionsAPI"); for (const key of pluginKeysToBind) { - p[key] &&= p[key].bind(p) as any; + p[key] &&= (p[key] as Function).bind(p) as any; } } diff --git a/src/api/ProfileCollections.tsx b/src/api/ProfileCollections.tsx index 07afef0cb..81cf4aa39 100644 --- a/src/api/ProfileCollections.tsx +++ b/src/api/ProfileCollections.tsx @@ -14,11 +14,12 @@ const logger = new Logger("ProfileCollectionAPI"); export type ProfileCollectionFactory = (props: { user: User; isSideBar: boolean; }) => ReactNode | Promise | null; -interface CollectionEntry { +export interface ProfileCollectionData { render: ProfileCollectionFactory; + priority: number; } -const profileCollections = new Map(); +const profileCollections = new Map(); const profileCollectionListeners = new Set<() => void>(); /** @@ -34,7 +35,7 @@ const profileCollectionListeners = new Set<() => void>(); * )); */ export function addProfileCollection(id: string, render: ProfileCollectionFactory, priority = 0) { - profileCollections.set(id, { render }); + profileCollections.set(id, { render, priority }); profileCollectionListeners.forEach(listener => listener()); } @@ -58,6 +59,7 @@ function ProfileCollections({ props }: { props: any; }) { }, []); return Array.from(profileCollections) + .sort(([, a], [, b]) => b.priority - a.priority) .map(([id, { render: Collection }]) => ( logger.error(`Failed to render profile collection: ${id}`, e.error)}> diff --git a/src/api/ProfileSections.tsx b/src/api/ProfileSections.tsx index ecf52ae9c..b1942602a 100644 --- a/src/api/ProfileSections.tsx +++ b/src/api/ProfileSections.tsx @@ -13,12 +13,12 @@ const logger = new Logger("ProfileSectionsAPI"); export type ProfileSectionFactory = (props: { userId: string; isSideBar: boolean; }) => ReactNode | Promise | null; -interface SectionEntry { +export interface ProfileSectionData { render: ProfileSectionFactory; priority: number; } -const profileSections = new Map(); +const profileSections = new Map(); const profileSectionListeners = new Set<() => void>(); /** diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 9e674c326..555f4486f 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -70,6 +70,7 @@ export interface Settings { | "under-page" | "window" | undefined; + windowsMaterial: "none" | "mica" | "tabbed" | "acrylic"; disableMinSize: boolean; winNativeTitleBar: boolean; plugins: { @@ -126,6 +127,7 @@ const DefaultSettings: Settings = { transparent: false, winCtrlQ: false, macosVibrancyStyle: undefined, + windowsMaterial: "none", disableMinSize: false, winNativeTitleBar: false, plugins: {}, diff --git a/src/api/SettingsSync/cloudSetup.tsx b/src/api/SettingsSync/cloudSetup.tsx index b1fd1a7f4..0d0496f44 100644 --- a/src/api/SettingsSync/cloudSetup.tsx +++ b/src/api/SettingsSync/cloudSetup.tsx @@ -8,9 +8,8 @@ import * as DataStore from "@api/DataStore"; import { showNotification } from "@api/Notifications"; import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; -import { openModal } from "@utils/modal"; import { t } from "@utils/translation"; -import { OAuth2AuthorizeModal, UserStore } from "@webpack/common"; +import { OAuth2AuthorizeModal, openModal, UserStore } from "@webpack/common"; export const logger = new Logger("SettingsSync:CloudSetup", "#39b7e0"); diff --git a/src/components/Badge.css b/src/components/Badge.css index be4c3eb7a..e9153b18e 100644 --- a/src/components/Badge.css +++ b/src/components/Badge.css @@ -13,6 +13,7 @@ letter-spacing: 0.02em; text-align: center; text-transform: uppercase; + white-space: nowrap; } .vc-badge-icon { @@ -51,4 +52,4 @@ .vc-badge-premium { background-color: var(--premium-tier-2-purple-for-gradients); color: var(--white); -} +} \ No newline at end of file diff --git a/src/components/Card.css b/src/components/Card.css index bdd9ec7b0..8ec6c1fe6 100644 --- a/src/components/Card.css +++ b/src/components/Card.css @@ -11,6 +11,10 @@ color: var(--text-default); } +.vc-card-normal { + border: 1px solid var(--border-subtle); +} + .vc-card-danger { background-color: var(--background-feedback-critical); border-color: var(--background-feedback-critical); @@ -56,4 +60,4 @@ .vc-card-defaultPadding { padding: 1em; -} +} \ No newline at end of file diff --git a/src/components/ThemeSettings/ThemesTab.tsx b/src/components/ThemeSettings/ThemesTab.tsx index e5abe3c7b..0f2f84c98 100644 --- a/src/components/ThemeSettings/ThemesTab.tsx +++ b/src/components/ThemeSettings/ThemesTab.tsx @@ -37,7 +37,6 @@ import { classNameFactory } from "@utils/css"; import { copyWithToast, openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; -import { openModal } from "@utils/modal"; import { relaunch, showItemInFolder } from "@utils/native"; import { useAwaiter, useForceUpdater } from "@utils/react"; import type { ThemeHeader } from "@utils/themes"; @@ -46,7 +45,7 @@ import { usercssParse } from "@utils/themes/usercss"; import { t } from "@utils/translation"; import { getStylusWebStoreUrl } from "@utils/web"; import { findComponentByCodeLazy, findLazy } from "@webpack"; -import { Alerts, Menu, React, Select, showToast, TextInput, Toasts, Tooltip, useEffect, useMemo, useState } from "@webpack/common"; +import { Alerts, Menu, openModal,React, Select, showToast, TextInput, Toasts, Tooltip, useEffect, useMemo, useState } from "@webpack/common"; import { ContextMenuApi } from "@webpack/common/menu"; import type { ComponentType, Ref, SyntheticEvent } from "react"; import type { UserstyleHeader } from "usercss-meta"; diff --git a/src/components/ThemeSettings/UserCSSModal.tsx b/src/components/ThemeSettings/UserCSSModal.tsx index b4f3e4812..550eeb877 100644 --- a/src/components/ThemeSettings/UserCSSModal.tsx +++ b/src/components/ThemeSettings/UserCSSModal.tsx @@ -9,15 +9,15 @@ import { BaseText } from "@components/BaseText"; import { Flex } from "@components/Flex"; import { CopyIcon, PasteIcon, ResetIcon } from "@components/Icons"; import { copyWithToast } from "@utils/discord"; -import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; -import { showToast, Toasts, Tooltip } from "@webpack/common"; +import { RenderModalProps } from "@vencord/discord-types"; +import { Modal, showToast, Toasts, Tooltip } from "@webpack/common"; import { type ReactNode } from "react"; import { UserstyleHeader } from "usercss-meta"; import { SettingBooleanComponent, SettingColorComponent, SettingNumberComponent, SettingRangeComponent, SettingSelectComponent, SettingTextComponent } from "./components"; interface UserCSSSettingsModalProps { - modalProps: ModalProps; + modalProps: RenderModalProps; theme: UserstyleHeader; onSettingsReset: () => void; } @@ -186,21 +186,22 @@ export function UserCSSSettingsModal({ modalProps, theme, onSettingsReset }: Use } return ( - - - Settings for {theme.name} - - - - + + Settings for {theme.name} + + + + + - - - - - {controls} - - - + } + > + + {controls} + + ); } diff --git a/src/components/settings/AddonCard.css b/src/components/settings/AddonCard.css index ec0f7b326..c76184ee7 100644 --- a/src/components/settings/AddonCard.css +++ b/src/components/settings/AddonCard.css @@ -32,7 +32,7 @@ } .vc-addon-note { - height: 36px; + height: 34px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; @@ -105,4 +105,4 @@ .vc-addon-footer { font-size: small; -} +} \ No newline at end of file diff --git a/src/components/settings/tabs/BaseTab.tsx b/src/components/settings/tabs/BaseTab.tsx index 2e67c7d8a..47b52a9c0 100644 --- a/src/components/settings/tabs/BaseTab.tsx +++ b/src/components/settings/tabs/BaseTab.tsx @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; import { handleComponentFailed } from "@components/handleComponentFailed"; -import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { onlyOnce } from "@utils/onlyOnce"; import { t } from "@utils/translation"; +import { Modal, openModal } from "@webpack/common"; import type { ComponentType, PropsWithChildren } from "react"; export function SettingsTab({ children }: PropsWithChildren) { @@ -43,19 +42,18 @@ export function wrapTab(component: ComponentType, tab: string) { } export function openSettingsTabModal(Tab: ComponentType) { - try { - openModal(wrapTab((modalProps: ModalProps) => ( - - - {Tab.displayName?.replace("SettingsTab", "") || "Settings"} - - + Tab = wrapTab(Tab, Tab.displayName || "SettingsTab"); - - - - - ), Tab.displayName || "SettingsTab")); + try { + openModal(props => ( + + + + )); } catch { handleSettingsTabError(); } diff --git a/src/components/settings/tabs/patchHelper/PatchPreview.tsx b/src/components/settings/tabs/patchHelper/PatchPreview.tsx index 00f7f0597..e294280da 100644 --- a/src/components/settings/tabs/patchHelper/PatchPreview.tsx +++ b/src/components/settings/tabs/patchHelper/PatchPreview.tsx @@ -13,8 +13,8 @@ import { ReplaceFn } from "@utils/types"; import { Button, Parser, useMemo, useState } from "@webpack/common"; import type { Change } from "diff"; -// Do not include diff in non dev builds (side effects import) -if (IS_DEV) { +// Do not include diff in standalone builds (side effects import) +if (!IS_STANDALONE) { var differ = require("diff") as typeof import("diff"); } @@ -130,7 +130,10 @@ export function PatchPreview({ module, match, replacement, setReplacementError } className={Margins.top20} onClick={() => { try { - Function(patchedCode.replace(/^(?=function\()/, "0,")); + const isArrowFunction = patchedCode.startsWith("("); + const wrappedCode = "0," + (!isArrowFunction ? "function" : "") + patchedCode.slice(patchedCode.indexOf("(")); + Function(wrappedCode); + setCompileResult([true, "Compiled successfully"]); } catch (err) { setCompileResult([false, (err as Error).message]); diff --git a/src/components/settings/tabs/patchHelper/index.tsx b/src/components/settings/tabs/patchHelper/index.tsx index b0701a1ac..64e333559 100644 --- a/src/components/settings/tabs/patchHelper/index.tsx +++ b/src/components/settings/tabs/patchHelper/index.tsx @@ -183,4 +183,4 @@ function PatchHelper() { ); } -export default IS_DEV ? wrapTab(PatchHelper, "Patch Helper") : null; +export default !IS_STANDALONE ? wrapTab(PatchHelper, "PatchHelper") : null; diff --git a/src/components/settings/tabs/plugins/ContributorModal.css b/src/components/settings/tabs/plugins/ContributorModal.css index 9cfcb4b5a..e63579fa8 100644 --- a/src/components/settings/tabs/plugins/ContributorModal.css +++ b/src/components/settings/tabs/plugins/ContributorModal.css @@ -1,12 +1,12 @@ .vc-author-modal-root { - padding: 1em; + padding: 0 0 16px; } .vc-author-modal-header { display: flex; align-items: center; gap: 8px; - margin-bottom: 1em; + margin-bottom: 4px; } .vc-author-modal-name { @@ -50,18 +50,16 @@ 0 2px 6px rgb(0 0 0 / 35%); } -.vc-author-modal-root+[class*="footer"] { - padding: 16px; -} - .vc-author-modal-links { display: flex; gap: 4px; margin-left: auto; + width: 100%; + justify-content: flex-end; } .vc-author-modal-plugins { display: grid; gap: 0.5em; margin-top: 0.75em; -} +} \ No newline at end of file diff --git a/src/components/settings/tabs/plugins/ContributorModal.tsx b/src/components/settings/tabs/plugins/ContributorModal.tsx index 38b48c9bd..3e2bb8cd0 100644 --- a/src/components/settings/tabs/plugins/ContributorModal.tsx +++ b/src/components/settings/tabs/plugins/ContributorModal.tsx @@ -7,17 +7,15 @@ import "./ContributorModal.css"; import { useSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { HeadingPrimary } from "@components/Heading"; +import { Heading } from "@components/Heading"; import { Link } from "@components/Link"; import { Paragraph } from "@components/Paragraph"; import { EquicordDevsById, VencordDevsById } from "@utils/constants"; import { classNameFactory } from "@utils/css"; import { fetchUserProfile } from "@utils/discord"; -import { ModalContent, ModalFooter, ModalRoot, openModal } from "@utils/modal"; import { Translate } from "@utils/translation"; -import { User } from "@vencord/discord-types"; -import { showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common"; +import { RenderModalProps, User } from "@vencord/discord-types"; +import { Modal, openModal, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common"; import Plugins, { PluginMeta } from "~plugins"; @@ -27,16 +25,10 @@ import { PluginCard } from "./PluginCard"; const cl = classNameFactory("vc-author-modal-"); export function openContributorModal(user: User) { - openModal(modalProps => - - - - - - ); + openModal(modalProps => ); } -function ContributorModal({ user }: { user: User; }) { +function ContributorModal({ user, modalProps }: { user: User; modalProps: RenderModalProps; }) { useSettings(); const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id)); @@ -68,18 +60,20 @@ function ContributorModal({ user }: { user: User; }) { const hasLinks = website || githubName; return ( - <> - + - {user.username} + {user.username}
- - {plugins.length ? ( + } + subtitle={ + plugins.length ? ( - )} - - {!!plugins.length && ( -
- {plugins.map(p => - showToast("Restart to apply changes!")} - /> - )} -
- )} - - - {hasLinks && ( - -
+ ) + } + actionBarInput={ + hasLinks && ( +
{website && ( )}
- - )} - + ) + } + > +
+ {!!plugins.length && ( +
+ {plugins.map(p => + showToast("Restart to apply changes!")} + /> + )} +
+ )} +
+ ); } diff --git a/src/components/settings/tabs/plugins/PluginModal.css b/src/components/settings/tabs/plugins/PluginModal.css index bdd35c80c..0983f505a 100644 --- a/src/components/settings/tabs/plugins/PluginModal.css +++ b/src/components/settings/tabs/plugins/PluginModal.css @@ -1,7 +1,6 @@ .vc-plugin-modal-header { align-items: flex-start; position: relative; - padding: 16px 16px 0 24px; } .vc-plugin-modal-header-content { @@ -70,7 +69,7 @@ display: flex; flex-wrap: wrap; gap: 4px; - margin-block: 8px 16px; + margin-top: 8px; } .vc-plugin-modal-tag { @@ -82,3 +81,9 @@ padding: 6px 12px; color: var(--text-default); } + +.vc-plugin-modal-about-box { + padding: 16px; + border-radius: 8px; + border: 1px solid var(--border-normal, var(--background-modifier-accent)); +} \ No newline at end of file diff --git a/src/components/settings/tabs/plugins/PluginModal.tsx b/src/components/settings/tabs/plugins/PluginModal.tsx index 186f2be1f..5d397afb5 100644 --- a/src/components/settings/tabs/plugins/PluginModal.tsx +++ b/src/components/settings/tabs/plugins/PluginModal.tsx @@ -32,11 +32,10 @@ import { classNameFactory } from "@utils/css"; import { proxyLazy } from "@utils/lazy"; import { Margins } from "@utils/margins"; import { classes, isObjectEmpty } from "@utils/misc"; -import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { OptionType, Plugin, PluginTag } from "@utils/types"; -import { User } from "@vencord/discord-types"; +import { RenderModalProps, User } from "@vencord/discord-types"; import { findComponentByCodeLazy, findCssClassesLazy } from "@webpack"; -import { Clickable, FluxDispatcher, React, Toasts, Tooltip, useEffect, useMemo, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common"; +import { Clickable, FluxDispatcher, Modal, openModal, React, Text, Toasts, Tooltip, useEffect, useMemo, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common"; import { Constructor } from "type-fest"; import { PluginMeta } from "~plugins"; @@ -53,7 +52,7 @@ const ConfirmModal = findComponentByCodeLazy('parentComponent:"ConfirmModal"'); const WarningIcon = findComponentByCodeLazy("3.15H3.29c-1.74"); const UserRecord: Constructor> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any; -interface PluginModalProps extends ModalProps { +interface PluginModalProps extends RenderModalProps { plugin: Plugin; onRestartNeeded(key: string): void; } @@ -142,6 +141,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti onChange={debounce(onChange)} pluginSettings={pluginSettings} definedSettings={settings} + closePluginSettings={onClose} /> ); @@ -176,28 +176,36 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti const isEquicordPlugin = pluginMeta.folderName.startsWith("src/equicordplugins/") ?? false; return ( - - -
- {plugin.name} - {plugin.description} - {!!plugin.tags?.length && } - {!!plugin.settingsAboutComponent && ( -
- - - -
- )} + + {plugin.name}
-
- + } + subtitle={ +
+
+ {plugin.description} + {!!plugin.tags?.length && } +
- - - + } + > + {!!plugin.settingsAboutComponent && ( +
+
+ + + +
+
+ )} +
- Authors + Authors
Settings {renderSettings()}
- - +
+
{hasSettings ? ( @@ -262,8 +270,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti )} - - +
+ ); } diff --git a/src/components/settings/tabs/plugins/UIElements.css b/src/components/settings/tabs/plugins/UIElements.css index 30db51dbd..a362aad6a 100644 --- a/src/components/settings/tabs/plugins/UIElements.css +++ b/src/components/settings/tabs/plugins/UIElements.css @@ -18,10 +18,9 @@ } .vc-plugin-ui-elements-modal-content { - padding: 1em; display: flex; flex-direction: column; - gap: 1.5em; + gap: 2em; } .vc-plugin-ui-elements-switches { diff --git a/src/components/settings/tabs/plugins/UIElements.tsx b/src/components/settings/tabs/plugins/UIElements.tsx index 6d79c96cb..a3a7bb8e2 100644 --- a/src/components/settings/tabs/plugins/UIElements.tsx +++ b/src/components/settings/tabs/plugins/UIElements.tsx @@ -17,9 +17,9 @@ import { Switch } from "@components/Switch"; import { classNameFactory } from "@utils/css"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; -import { ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { IconComponent } from "@utils/types"; -import { Clickable } from "@webpack/common"; +import { RenderModalProps } from "@vencord/discord-types"; +import { Clickable, Modal,openModal } from "@webpack/common"; const cl = classNameFactory("vc-plugin-ui-elements-"); @@ -74,7 +74,7 @@ function Section(props: { return (
- {title} + {title} {description}
@@ -89,12 +89,12 @@ function Section(props: { ); } -function UIElementsModal(props: ModalProps) { +function UIElementsModal(props: RenderModalProps) { const { uiElements } = useSettings(["uiElements.*"]); return ( - - + +
- - +
+
); } diff --git a/src/components/settings/tabs/plugins/components/Common.tsx b/src/components/settings/tabs/plugins/components/Common.tsx index 803f1d395..ab5454d89 100644 --- a/src/components/settings/tabs/plugins/components/Common.tsx +++ b/src/components/settings/tabs/plugins/components/Common.tsx @@ -22,6 +22,7 @@ interface SettingBaseProps { }; id: string; definedSettings: DefinedSettings; + closePluginSettings(): void; } export type SettingProps = SettingBaseProps; diff --git a/src/components/settings/tabs/plugins/components/ComponentSetting.tsx b/src/components/settings/tabs/plugins/components/ComponentSetting.tsx index e1521f02a..d67f83be7 100644 --- a/src/components/settings/tabs/plugins/components/ComponentSetting.tsx +++ b/src/components/settings/tabs/plugins/components/ComponentSetting.tsx @@ -20,6 +20,6 @@ import { PluginSettingComponentDef } from "@utils/types"; import { ComponentSettingProps } from "./Common"; -export function ComponentSetting({ setting, onChange }: ComponentSettingProps) { - return setting.component({ setValue: onChange, option: setting }); +export function ComponentSetting({ setting, onChange, closePluginSettings }: ComponentSettingProps) { + return setting.component({ setValue: onChange, option: setting, closePluginSettings }); } diff --git a/src/components/settings/tabs/plugins/index.tsx b/src/components/settings/tabs/plugins/index.tsx index 2909f15ab..80686ecae 100644 --- a/src/components/settings/tabs/plugins/index.tsx +++ b/src/components/settings/tabs/plugins/index.tsx @@ -35,10 +35,10 @@ import { isTruthy } from "@utils/guards"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; -import { useAwaiter, useIntersection } from "@utils/react"; +import { useAwaiter, useCleanupEffect, useIntersection } from "@utils/react"; import { t } from "@utils/translation"; import { PluginTag, PluginTags } from "@utils/types"; -import { Alerts, lodash, Parser, React, SearchableSelect, Select, TextInput, Toasts, Tooltip, useCallback, useMemo, useRef, useState } from "@webpack/common"; +import { Alerts, ConfirmModal, lodash, openModal, Parser, React, SearchableSelect, Select, TextInput, Toasts, Tooltip, useCallback, useMemo, useRef, useState } from "@webpack/common"; import { JSX } from "react"; import Plugins, { ExcludedPlugins, PluginMeta } from "~plugins"; @@ -148,7 +148,7 @@ export default function PluginSettings() { const changeRef = useRef>(null); const changes = changeRef.current ??= new ChangeList(); - React.useEffect(() => { + useCleanupEffect(() => { return () => { if (!changes.hasChanges) return; @@ -158,23 +158,29 @@ export default function PluginSettings() { const displayed = pluginNames.slice(0, maxDisplay); const remainingCount = pluginNames.length - displayed.length; - Alerts.show({ - title: t("vencord.settings.restartRequired.title"), - body: ( -
- {displayed.map((s, i) => ( - - {i > 0 && ", "} - {Parser.parse("`" + s + "`")} - - ))} - {remainingCount > 0 && and {remainingCount} more} -
- ), - confirmText: t("vencord.settings.restartRequired.confirm"), - cancelText: t("vencord.settings.restartRequired.cancel"), - onConfirm: () => location.reload() - }); + openModal(props => ( + location.reload()} + > + <> +

{t("vencord.settings.restartRequired.plugins")}

+
+ {displayed.map((s, i) => ( + + {i > 0 && ", "} + {Parser.parse("`" + s + "`")} + + ))} + {remainingCount > 0 && and {remainingCount} more} +
+ +
+ )); }; }, []); diff --git a/src/components/settings/tabs/styles.css b/src/components/settings/tabs/styles.css index 318bb3215..c87965b1a 100644 --- a/src/components/settings/tabs/styles.css +++ b/src/components/settings/tabs/styles.css @@ -88,12 +88,4 @@ .vc-cloud-button-icon { height: 1.25em; -} - -.vc-settings-modal-root { - background: var(--background-base-low); -} - -.vc-settings-modal { - padding-bottom: 1em; } \ No newline at end of file diff --git a/src/components/settings/tabs/updater/Components.tsx b/src/components/settings/tabs/updater/Components.tsx index 22f830eb9..88893d43f 100644 --- a/src/components/settings/tabs/updater/Components.tsx +++ b/src/components/settings/tabs/updater/Components.tsx @@ -14,7 +14,7 @@ import { Span } from "@components/Span"; import { Margins } from "@utils/margins"; import { relaunch } from "@utils/native"; import { changes, checkForUpdates, update, updateError } from "@utils/updater"; -import { Alerts, React, Toasts, useState } from "@webpack/common"; +import { ConfirmModal, openModal, React, Toasts, useState } from "@webpack/common"; import { runWithDispatch } from "./runWithDispatch"; @@ -113,17 +113,21 @@ export function Updatable(props: CommonProps) { setUpdates([]); await new Promise(r => { - Alerts.show({ - title: "Update Success!", - body: "Successfully updated. Restart now to apply the changes?", - confirmText: "Restart", - cancelText: "Not now!", - onConfirm() { - relaunch(); - r(); - }, - onCancel: r - }); + openModal(props => ( + { + relaunch(); + r(); + }} + onCancel={r} + /> + )); }); } })} diff --git a/src/components/settings/tabs/updater/runWithDispatch.tsx b/src/components/settings/tabs/updater/runWithDispatch.tsx index 719958e78..55f290ad9 100644 --- a/src/components/settings/tabs/updater/runWithDispatch.tsx +++ b/src/components/settings/tabs/updater/runWithDispatch.tsx @@ -6,7 +6,7 @@ import { ErrorCard } from "@components/ErrorCard"; import { UpdateLogger } from "@utils/updater"; -import { Alerts, Parser } from "@webpack/common"; +import { ConfirmModal,openModal, Parser } from "@webpack/common"; function getErrorMessage(e: any) { if (!e?.code || !e.cmd) @@ -33,16 +33,20 @@ export function runWithDispatch(dispatch: React.Dispatch ( + {err.split("\n").map((line, idx) =>
{Parser.parse(line)}
)}
- ) - }); +
+ )); } finally { dispatch(false); } diff --git a/src/components/settings/tabs/vencord/MacVibrancySettings.tsx b/src/components/settings/tabs/vencord/MacVibrancySettings.tsx index 015f27588..157b16ab6 100644 --- a/src/components/settings/tabs/vencord/MacVibrancySettings.tsx +++ b/src/components/settings/tabs/vencord/MacVibrancySettings.tsx @@ -7,80 +7,81 @@ import { useSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Heading } from "@components/Heading"; +import { IS_MAC } from "@utils/constants"; import { Margins } from "@utils/margins"; import { identity } from "@utils/misc"; import { t } from "@utils/translation"; import { Select } from "@webpack/common"; -export function VibrancySettings() { +export function MacOSVibrancySettings() { const settings = useSettings(["macosVibrancyStyle"]); + if (!IS_MAC || IS_WEB) return null; + return ( - <> - {t("vencord.settings.macosVibrancyStyle.title")} - - settings.macosVibrancyStyle = v} + isSelected={v => settings.macosVibrancyStyle === v} + serialize={identity} + /> + ); } diff --git a/src/components/settings/tabs/vencord/NotificationSettings.tsx b/src/components/settings/tabs/vencord/NotificationSettings.tsx index bad4773e8..acc992985 100644 --- a/src/components/settings/tabs/vencord/NotificationSettings.tsx +++ b/src/components/settings/tabs/vencord/NotificationSettings.tsx @@ -6,7 +6,6 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog"; import { useSettings } from "@api/Settings"; -import { BaseText } from "@components/BaseText"; import { Button } from "@components/Button"; import { ErrorCard } from "@components/ErrorCard"; import { Flex } from "@components/Flex"; @@ -15,9 +14,8 @@ import { Heading } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; import { Margins } from "@utils/margins"; import { identity } from "@utils/misc"; -import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { t } from "@utils/translation"; -import { Select, Slider } from "@webpack/common"; +import { Modal, openModal, Select, Slider } from "@webpack/common"; export function NotificationSection() { return ( @@ -41,16 +39,13 @@ export function NotificationSection() { export function openNotificationSettingsModal() { openModal(props => ( - - - Notification Settings - - - - - - - + + + )); } @@ -58,8 +53,8 @@ function NotificationSettings() { const settings = useSettings(["notifications.*"]).notifications; return ( -
- Notification Style + <> + Notification Style {settings.useNative !== "never" && Notification?.permission === "denied" && ( Desktop Notification Permission denied @@ -135,6 +130,6 @@ function NotificationSettings() { onValueRender={v => v === 200 ? "∞" : v} onMarkerRender={v => v === 200 ? "∞" : v} /> -
+ ); } diff --git a/src/components/settings/tabs/vencord/WindowsMaterialSettings.tsx b/src/components/settings/tabs/vencord/WindowsMaterialSettings.tsx new file mode 100644 index 000000000..0fec4ef88 --- /dev/null +++ b/src/components/settings/tabs/vencord/WindowsMaterialSettings.tsx @@ -0,0 +1,55 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { useSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Heading } from "@components/Heading"; +import { Margins } from "@components/margins"; +import { Paragraph } from "@components/Paragraph"; +import { IS_WINDOWS } from "@utils/constants"; +import { Select } from "@webpack/common"; + +export function WindowsMaterialSettings() { + const settings = useSettings(["windowsMaterial"]); + + if (!IS_WINDOWS || IS_WEB || !VencordNative.native.supportsWindowsMaterial()) return null; + + return ( + + Background Material + + Windows transparent background effects. You need a theme that supports transparency or this will do nothing. A restart is required after changing this setting. + + + (settings.macosVibrancyStyle = v)} - isSelected={v => settings.macosVibrancyStyle === v} - serialize={identity} - /> - - )} - - - - Notifications - - Configure how Equicord handles notifications. You can customize when and how you receive alerts, or view a history of past notifications. - - - - - - - + + ); } diff --git a/src/equicordplugins/_api/profileCollections.ts b/src/equicordplugins/_api/profileCollections.ts index bbc51a840..f573c006a 100644 --- a/src/equicordplugins/_api/profileCollections.ts +++ b/src/equicordplugins/_api/profileCollections.ts @@ -12,6 +12,7 @@ export default definePlugin({ description: "API to add collections to the user profile panel like discords game collection.", authors: [Devs.thororen], patches: [ + // message and member list popouts { find: "#{intl::USER_PROFILE_FRIEND_REQUEST_TOAST}", replacement: { @@ -19,6 +20,7 @@ export default definePlugin({ replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections(arguments[0]),", } }, + // user panel popout { find: '"UserProfileAccountPopout"', replacement: { @@ -26,6 +28,7 @@ export default definePlugin({ replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections(arguments[0]),", }, }, + // dm sidebar { find: ".SIDEBAR,disableToolbar:", replacement: { diff --git a/src/equicordplugins/_api/profileSections.ts b/src/equicordplugins/_api/profileSections.ts index 1d7520c79..b59e0606a 100644 --- a/src/equicordplugins/_api/profileSections.ts +++ b/src/equicordplugins/_api/profileSections.ts @@ -12,15 +12,15 @@ export default definePlugin({ description: "API to add sections near the 'Member Since' area of user profile panels.", authors: [Devs.thororen], patches: [ - // DM User Sidebar + // dm user sidebar { - find: "#{intl::PREMIUM_GIFTING_BUTTON}),action:", + find: '"UserProfileSidebar"', replacement: { match: /(#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id)}\)}\))/, replace: "$1,Vencord.Api.ProfileSections.renderProfileSections({userId:$2,isSideBar:true})", } }, - // User Profile Modal + // user profile modal { find: ",applicationRoleConnection:", replacement: { @@ -28,7 +28,7 @@ export default definePlugin({ replace: "$1,Vencord.Api.ProfileSections.renderProfileSections({userId:$2,isSideBar:false}),", } }, - // User Profile Modal v2 + // user profile modal v2 { find: ".MODAL_V2,onClose:", replacement: { diff --git a/src/equicordplugins/_api/userArea.ts b/src/equicordplugins/_api/userArea.ts index 9519f2efc..acb5abdea 100644 --- a/src/equicordplugins/_api/userArea.ts +++ b/src/equicordplugins/_api/userArea.ts @@ -11,7 +11,7 @@ import { t } from "@utils/translation"; import definePlugin from "@utils/types"; import { findCssClassesLazy } from "@webpack"; -const { iconForeground } = findCssClassesLazy("iconForeground", "accountPopoutButtonWrapper"); +const accountClasses = findCssClassesLazy("iconForeground", "accountPopoutButtonWrapper"); export default definePlugin({ name: "UserAreaAPI", @@ -38,7 +38,7 @@ export default definePlugin({ renderButtons(props: { nameplate?: any; }) { return Vencord.Api.UserArea._renderButtons({ nameplate: props.nameplate, - iconForeground: props.nameplate != null ? iconForeground : void 0, + iconForeground: accountClasses.iconForeground, hideTooltips: this.shouldHideTooltips() }); }, diff --git a/src/equicordplugins/_core/concatenatedModules.tsx b/src/equicordplugins/_core/concatenatedModules.tsx index 0cde5f7be..c6b4a6662 100644 --- a/src/equicordplugins/_core/concatenatedModules.tsx +++ b/src/equicordplugins/_core/concatenatedModules.tsx @@ -14,19 +14,20 @@ export default definePlugin({ name: "ConcatenatedModules", description: "Extract modules that have been concatenated by the bundler", authors: [Devs.thororen], - - required: true, - patches: [ { find: "AngleBracketsIcon", replacement: { match: /\i\.\i\((\i)\),\i\.\i\(\i,\{AIcon/, - replace: "$self.iconsModule($1),$&" + replace: "$self.setIconsModule($1),$&" } } ], - iconsModule(value: IconsDef) { + setIconsModule(value: IconsDef) { iconsModule = value; + + // incase you dont want to use iconviewer you can do + // Vencord.Plugins.plugins.ConcatenatedModules.iconsModule instead for icons and viewing paths + this.iconsModule = value; }, }); diff --git a/src/equicordplugins/baseDecoder/index.tsx b/src/equicordplugins/baseDecoder/index.tsx index 41233369d..bdbcdf148 100644 --- a/src/equicordplugins/baseDecoder/index.tsx +++ b/src/equicordplugins/baseDecoder/index.tsx @@ -17,17 +17,14 @@ */ import { definePluginSettings } from "@api/Settings"; -import { BaseText } from "@components/BaseText"; import { CodeBlock } from "@components/CodeBlock"; import ErrorBoundary from "@components/ErrorBoundary"; -import { Flex } from "@components/Flex"; import { Heading } from "@components/Heading"; import { EquicordDevs } from "@utils/constants"; import { copyWithToast } from "@utils/discord"; -import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { t } from "@utils/translation"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ChannelStore } from "@webpack/common"; +import { ChannelStore, Modal, openModal } from "@webpack/common"; function DecodeIcon() { return ( @@ -65,32 +62,26 @@ function decodeBase64Strings(base64Strings) { } function openDecodedBase64Modal(decodedContent) { - const key = openModal(props => ( + openModal(props => ( - - - {t("equicord.baseDecoder.ui.modalTitle")} - closeModal(key)} /> - - -
- {t("equicord.baseDecoder.ui.decodedContent")} - {decodedContent.map((content, index) => ( - - ))} -
-
- - - {decodedContent.map((content, index) => ( - - ))} - - -
-
+ ({ + text: `${t("equicord.baseDecoder.ui.copyDecoded")} ${index + 1}`, + variant: "primary", + onClick: () => copyWithToast(content, t("equicord.baseDecoder.ui.copiedToast")) + }))} + > +
+ {t("equicord.baseDecoder.ui.decodedContent")} + {decodedContent.map((content, index) => ( + + ))} +
+
+
)); } diff --git a/src/equicordplugins/blockKeywords/index.tsx b/src/equicordplugins/blockKeywords/index.tsx new file mode 100644 index 000000000..d10bb808e --- /dev/null +++ b/src/equicordplugins/blockKeywords/index.tsx @@ -0,0 +1,193 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { definePluginSettings } from "@api/Settings"; +import { Card } from "@components/Card"; +import { HeadingTertiary } from "@components/Heading"; +import { ErrorBoundary } from "@components/index"; +import { Margins } from "@components/margins"; +import { EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; +import { classes } from "@utils/misc"; +import definePlugin, { OptionType } from "@utils/types"; +import { Message } from "@vencord/discord-types"; +import { React, TextInput } from "@webpack/common"; + +let blockedKeywords: Array; +const cl = classNameFactory("vc-block-keywords-"); + +function splitPatterns(input: string): string[] { + return input + .replace(/\[([^\]]*)\]/g, m => m.replace(/,/g, "\x00")) // protect commas in [...] + .replace(/\{(\d+,?\d*|,\d+)\}/g, m => m.replace(",", "\x00")) // protect commas in {n,m} + .split(",") + .map(s => s.replace(/\x00/g, ",").trim()) + .filter(Boolean); +} + +function RegexHelper() { + const [testInput, setTestInput] = React.useState(""); + const { blockedWords, caseSensitive, useRegex } = settings.use(["blockedWords", "caseSensitive", "useRegex"]); + + const results = React.useMemo(() => { + const caseSensitiveFlag = caseSensitive ? "" : "i"; + return splitPatterns(blockedWords) + .map(pattern => { + try { + const regex = useRegex + ? new RegExp(pattern, caseSensitiveFlag) + : new RegExp(`\\b${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, caseSensitiveFlag); + return { pattern, matches: regex.test(testInput) }; + } catch (e: unknown) { + return { pattern, matches: false, error: e instanceof Error ? e.message : String(e) }; + } + }); + }, [testInput, blockedWords, caseSensitive, useRegex]); + + return ( + + Regex Helper + + {results.length === 0 ? + + No patterns configured + : ( + results.map(({ pattern, matches, error }, i) => ( + + {pattern} + {error && {error}} + + )))} + + ); +} + +const settings = definePluginSettings({ + blockedWords: { + type: OptionType.STRING, + description: "Comma-separated list of words to block", + default: "", + restartNeeded: true + }, + useRegex: { + type: OptionType.BOOLEAN, + description: "Use each value as a regular expression when checking message content (advanced)", + default: false, + restartNeeded: true + }, + regexHelper: { + type: OptionType.COMPONENT, + description: "Test your regular expressions against a sample input", + component: () => , + }, + caseSensitive: { + type: OptionType.BOOLEAN, + description: "Whether to use a case sensitive search or not", + default: false, + restartNeeded: true + }, + ignoreBlockedMessages: { + description: "Completely ignores (recent) new messages bar", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true, + }, +}, { + regexHelper: { + hidden() { return !this.store.useRegex; } + } +}); + +export function containsBlockedKeywords(message: Message) { + if (!blockedKeywords) return false; + + // test a nullable string against all keywords + const testField = (text: string | null | undefined) => text != null && blockedKeywords.some(regex => regex.test(text)); + + return blockedKeywords.some(regex => + regex.test(message.content)) || message.embeds.some(embed => + testField(embed.rawDescription) || testField(embed.rawTitle)); +} + +export default definePlugin({ + name: "BlockKeywords", + description: "Blocks messages containing specific user-defined keywords, as if the user sending them was blocked.", + tags: ["Appearance", "Customisation", "Privacy"], + authors: [EquicordDevs.catcraft, EquicordDevs.secp192k1], + patches: [ + { + find: "_channelMessages={}", + predicate: () => settings.store.blockedWords !== "", + replacement: { + match: /static commit\((\i)\)\{/g, + replace: "$&$1=$self.blockMessagesWithKeywords($1);" + } + }, + { + find: '"MessageStore"', + predicate: () => settings.store.ignoreBlockedMessages && settings.store.blockedWords !== "", + replacement: [ + { + match: /(?<=MESSAGE_CREATE:function\((\i)\){)/, + replace: (_, props) => `if($self.containsBlockedKeywords(${props}.message))return;` + } + ] + }, + { + find: '"ReadStateStore"', + predicate: () => settings.store.ignoreBlockedMessages && settings.store.blockedWords !== "", + replacement: [ + { + match: /(?<=MESSAGE_CREATE:function\((\i)\){)/, + replace: (_, props) => `if($self.containsBlockedKeywords(${props}.message))return;` + } + ] + }, + ], + + settings, + containsBlockedKeywords, + + start() { + const blockedWordsList = splitPatterns(settings.store.blockedWords); + const caseSensitiveFlag = settings.store.caseSensitive ? "" : "i"; + + if (blockedWordsList.length === 0) return; + + if (settings.store.useRegex) { + blockedKeywords = blockedWordsList.map(word => { + return new RegExp(word, caseSensitiveFlag); + }); + } else { + blockedKeywords = blockedWordsList.map(word => { + // escape regex chars in word https://stackoverflow.com/a/6969486 + return new RegExp(`\\b${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, caseSensitiveFlag); + }); + } + }, + + blockMessagesWithKeywords(messageList) { + return messageList.reset(messageList.map( + message => message.set("blocked", message.blocked || this.containsBlockedKeywords(message)) + )); + } +}); diff --git a/src/equicordplugins/blockKeywords/styles.css b/src/equicordplugins/blockKeywords/styles.css new file mode 100644 index 000000000..9e89d078a --- /dev/null +++ b/src/equicordplugins/blockKeywords/styles.css @@ -0,0 +1,16 @@ +.vc-block-keywords-regex { + padding: 0.75em +} + +.vc-block-keywords-card { + padding: 0.4em 0.75em; + display: flex; + align-items: center; + justify-content: space-between; +} + +.vc-block-keywords-error { + font-size: 12px; + margin-left: 1em; + flex-shrink: 0; +} \ No newline at end of file diff --git a/src/equicordplugins/channelTabs/components/BookmarkContainer.tsx b/src/equicordplugins/channelTabs/components/BookmarkContainer.tsx index b671049b8..16c46575e 100644 --- a/src/equicordplugins/channelTabs/components/BookmarkContainer.tsx +++ b/src/equicordplugins/channelTabs/components/BookmarkContainer.tsx @@ -10,9 +10,8 @@ import { CircleQuestionIcon, DiscoveryIcon, EnvelopeIcon, FriendsIcon, NitroIcon import { classNameFactory } from "@utils/css"; import { getGuildAcronym, getIntlMessage } from "@utils/discord"; import { classes } from "@utils/misc"; -import { closeModal, openModal } from "@utils/modal"; import { findComponentByCodeLazy } from "@webpack"; -import { Avatar, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, React, ReadStateStore, ReadStateUtils, SelectedChannelStore, SelectedGuildStore, TextInput, Tooltip, useDrag, useDrop, useEffect, useRef, UserStore, useState } from "@webpack/common"; +import { Avatar, ChannelStore, closeModal, ContextMenuApi, FluxDispatcher, GuildStore, Menu, openModal,React, ReadStateStore, ReadStateUtils, SelectedChannelStore, SelectedGuildStore, TextInput, Tooltip, useDrag, useDrop, useEffect, useRef, UserStore, useState } from "@webpack/common"; import { NotificationDot } from "./ChannelTab"; import { BookmarkContextMenu, EditModal } from "./ContextMenus"; diff --git a/src/equicordplugins/channelTabs/components/ContextMenus.tsx b/src/equicordplugins/channelTabs/components/ContextMenus.tsx index c93b05fbe..780f8b710 100644 --- a/src/equicordplugins/channelTabs/components/ContextMenus.tsx +++ b/src/equicordplugins/channelTabs/components/ContextMenus.tsx @@ -11,9 +11,9 @@ import { bookmarkFolderColors, bookmarkPlaceholderName, closeOtherTabs, closeTab import { Bookmark, BookmarkFolder, Bookmarks, ChannelTabsProps, UseBookmarkMethods } from "@equicordplugins/channelTabs/util/types"; import { getIntlMessage } from "@utils/discord"; import { Margins } from "@utils/margins"; -import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; import { t } from "@utils/translation"; -import { Button, ChannelStore, ColorPicker, FluxDispatcher, Menu, ReadStateStore, ReadStateUtils, Select, TextInput, useMemo, useState } from "@webpack/common"; +import { RenderModalProps } from "@vencord/discord-types"; +import { Button, ChannelStore, closeModal, ColorPicker, FluxDispatcher, Menu, Modal, openModal, ReadStateStore, ReadStateUtils, Select, TextInput, useMemo, useState } from "@webpack/common"; const legacyFolderColors: Record = { "var(--channeltabs-red)": bookmarkFolderColors.Red, @@ -116,7 +116,7 @@ function FolderChipPreview({ name, color, iconName }: { name: string; color: str } function FolderIconPickerModal({ modalProps, modalKey, name, color, iconName, onSelect, onColorChange }: { - modalProps: ModalProps, + modalProps: RenderModalProps, modalKey: string, name: string, color: string, @@ -134,73 +134,72 @@ function FolderIconPickerModal({ modalProps, modalKey, name, color, iconName, on }, [iconNames, search]); return ( - - - Choose Folder Icon - - - Preview - - Icon Color -
- { - const nextColor = intToColor(value); - setLocalColor(nextColor); - onColorChange(nextColor); - }} - showEyeDropper={false} - /> -
- Search - Choose Folder Icon} + actions={[ + { + text: "Close", + variant: "secondary", + onClick: () => closeModal(modalKey) + } + ]} + > + Preview + + Icon Color +
+ { + const nextColor = intToColor(value); + setLocalColor(nextColor); + onColorChange(nextColor); + }} + showEyeDropper={false} /> -
- {filteredIconNames.map(name => ( - - ))} -
- - - - - +
+ Search + +
+ {filteredIconNames.map(name => ( + + ))} +
+ ); } @@ -264,7 +263,7 @@ function FolderAppearanceFields({ } export function EditModal({ modalProps, modalKey, bookmark, onSave }: { - modalProps: ModalProps, + modalProps: RenderModalProps, modalKey: string, bookmark: Bookmark | BookmarkFolder, onSave: (name: string, color: string, iconName?: string) => void; @@ -275,48 +274,47 @@ export function EditModal({ modalProps, modalKey, bookmark, onSave }: { const placeholder = bookmarkPlaceholderName(bookmark); return ( - - - Edit Bookmark - - - {isBookmarkFolder(bookmark) - ? Edit Bookmark} + actions={[ + { + text: "Save", + variant: "primary", + onClick: () => onSave(name || placeholder, color, iconName) + }, + { + text: t("vencord.cancel"), + variant: "secondary", + onClick: () => closeModal(modalKey) + } + ]} + > + {isBookmarkFolder(bookmark) + ? + : <> + Bookmark Name + - : <> - Bookmark Name - - } - - - - - - + } + ); } function AddToFolderModal({ modalProps, modalKey, bookmarks, onSave }: { - modalProps: any, + modalProps: RenderModalProps, modalKey: string, bookmarks: Bookmarks, onSave: (folderIndex: number, folderName: string, folderColor: string, iconName?: string) => void; @@ -327,80 +325,79 @@ function AddToFolderModal({ modalProps, modalKey, bookmarks, onSave }: { const [iconName, setIconName] = useState(); return ( - - - Add Bookmark to Folder - - - Select a folder - isBookmarkFolder(bookmark)) + .map(([index, bookmark]) => ({ + label: bookmark.name, + value: parseInt(index, 10) + })), + { + label: "Create one", + value: -1, + default: true + }]} + isSelected={v => v === folderIndex} + select={setIndex} + serialize={String} + /> + {folderIndex === -1 && } + ); } -function DeleteFolderConfirmationModal({ modalProps, modalKey, onConfirm }) { +function DeleteFolderConfirmationModal({ modalProps, modalKey, onConfirm }: { + modalProps: RenderModalProps, + modalKey: string, + onConfirm: () => void; +}) { return ( - - - Are you sure? - - - - Deleting a bookmark folder will also delete all bookmarks within it. - - - - - - - + Are you sure?} + actions={[ + { + text: "Delete", + variant: "critical-primary", + onClick: onConfirm + }, + { + text: t("vencord.cancel"), + variant: "secondary", + onClick: () => closeModal(modalKey) + } + ]} + > + + Deleting a bookmark folder will also delete all bookmarks within it. + + ); } diff --git a/src/equicordplugins/channelTabs/index.tsx b/src/equicordplugins/channelTabs/index.tsx index 75e115af7..d60bf9f68 100644 --- a/src/equicordplugins/channelTabs/index.tsx +++ b/src/equicordplugins/channelTabs/index.tsx @@ -50,7 +50,7 @@ export default definePlugin({ description: t("equicord.channelTabs.description"), tags: ["Appearance", "Customisation", "Organisation", "Servers"], authors: [Devs.TheSun, Devs.TheKodeToad, EquicordDevs.keifufu, Devs.Nickyux, EquicordDevs.DiabeloDEV, EquicordDevs.justjxke, EquicordDevs.keircn], - dependencies: ["ContextMenuAPI"], + dependencies: ["ContextMenuAPI", "ConcatenatedModules"], contextMenus: { "channel-mention-context": contextMenuPatch, "channel-context": contextMenuPatch, diff --git a/src/equicordplugins/clipsEnhancements.discordDesktop/index.tsx b/src/equicordplugins/clipsEnhancements.discordDesktop/index.tsx index 43f13ad84..2a81739cc 100644 --- a/src/equicordplugins/clipsEnhancements.discordDesktop/index.tsx +++ b/src/equicordplugins/clipsEnhancements.discordDesktop/index.tsx @@ -40,7 +40,7 @@ const settings = definePluginSettings({ }, enableSpeakingIndicators: { type: OptionType.BOOLEAN, - description: "Enable speaking indicators", + description: t("equicord.clipsEnhancements.settings.enableSpeakingIndicators"), default: true, restartNeeded: true }, @@ -104,14 +104,6 @@ export default definePlugin({ replace: "$self.patchTimeslots($&)" } }, - // enables clips - { - find: "2026-03-clips-experiment", - replacement: { - match: /defaultConfig:\{enableClips:!\d,ignorePlatformRestriction:!\d,enableScreenshotKeybind:!\d,enableVoiceOnlyClips:!\d,enableSpeakingIndicators:!\d,enableAdvancedSignals:!\d\}/, - replace: "defaultConfig:{enableClips:!0,ignorePlatformRestriction:$self.settings.store.ignorePlatformRestriction,enableScreenshotKeybind:$self.settings.store.enableScreenshotKeybind,enableVoiceOnlyClips:$self.settings.store.enableVoiceOnlyClips,enableSpeakingIndicators:$self.settings.store.enableSpeakingIndicators,enableAdvancedSignals:$self.settings.store.enableAdvancedSignals}" - } - } ], patchTimeslots(timeslots: { id: string; value: number; label: string; }[]) { diff --git a/src/equicordplugins/collapsibleUi/index.tsx b/src/equicordplugins/collapsibleUi/index.tsx deleted file mode 100644 index 7893bcb3e..000000000 --- a/src/equicordplugins/collapsibleUi/index.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { ChannelToolbarButton } from "@api/HeaderBar"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { EquicordDevs } from "@utils/constants"; -import { classNameFactory } from "@utils/css"; -import { classes } from "@utils/misc"; -import definePlugin from "@utils/types"; -import { ContextMenuApi, Menu } from "@webpack/common"; -import type { ReactNode, SVGProps } from "react"; - -import { PanelId, panelRegistry, settings, toolbarPanelOrder } from "./settings"; -import managedStyle from "./style.css?managed"; - -const cl = classNameFactory("vc-collapsible-ui-"); - -type StateStore = ReturnType; - -function PanelsIcon(props: SVGProps) { - return ( - - - - ); -} - -function getStateClasses(store: StateStore) { - const classNames = [cl("state"), cl(`collapsed-size-${store.collapsedSize}`), cl(`transition-${store.transitionMs}`)]; - - for (const panelId of toolbarPanelOrder) { - const panel = panelRegistry[panelId]; - if (store[panel.collapsedKey]) classNames.push(cl(`${panel.classId}-collapsed`)); - } - - return classes(...classNames); -} - -function togglePanel(panelId: PanelId) { - const key = panelRegistry[panelId].collapsedKey; - settings.store[key] = !settings.store[key]; -} - -const ToolbarMenu = ErrorBoundary.wrap(({ onClose }: { onClose(): void; }) => { - const store = settings.use(); - - return ( - - {toolbarPanelOrder.map(panelId => { - const panel = panelRegistry[panelId]; - const collapsed = store[panel.collapsedKey]; - - return ( - togglePanel(panelId)} - /> - ); - })} - - ); -}, { noop: true }); - -const ToolbarButtons = ErrorBoundary.wrap(() => { - const store = settings.use(); - const anyCollapsed = toolbarPanelOrder.some(panelId => store[panelRegistry[panelId].collapsedKey]); - - return ( - <> - - ContextMenuApi.openContextMenu(event, () => )} - onContextMenu={event => ContextMenuApi.openContextMenu(event, () => )} - /> - - ); -}, { noop: true }); - -const ChatButtonsRow = ErrorBoundary.wrap(({ buttons }: { buttons: ReactNode[]; }) => { - const { chatButtonsCollapsed } = settings.use(); - - if (buttons.length === 0) return <>{buttons}; - - return ( -
-
- {buttons} -
-
- ); -}, { noop: true }); - -export default definePlugin({ - name: "CollapsibleUI", - description: "Native collapsible channel, member, chat button, and user area surfaces.", - tags: ["Appearance", "Customisation", "Chat", "Servers"], - dependencies: ["HeaderBarAPI", "ChatInputButtonAPI"], - authors: [EquicordDevs.benjii], - searchTerms: ["ui", "sidebar", "layout"], - managedStyle, - settings, - - headerBarButton: { - icon: PanelsIcon, - location: "channeltoolbar", - priority: 25, - render: () => , - }, - - patches: [ - { - find: ".DISPLAY_NAME_STYLES_COACHMARK)", - replacement: { - match: /className:(\i\.\i),(?=style:(\i),children:\[)/, - replace: "className:$self.userAreaControlsClass($1)," - } - }, - ], - chatBarButtonWrapper: (buttons: ReactNode) => { - if (!Array.isArray(buttons) || buttons.length === 0) return buttons; - return ; - }, - - userAreaControlsClass(className: string) { - return classes(className, cl("user-area-controls")); - }, -}); diff --git a/src/equicordplugins/collapsibleUi/settings.ts b/src/equicordplugins/collapsibleUi/settings.ts deleted file mode 100644 index 84be9d54e..000000000 --- a/src/equicordplugins/collapsibleUi/settings.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { OptionType } from "@utils/types"; - -export const transitionMarkers = [0, 80, 120, 160, 200, 260, 320]; -export const collapsedSizeMarkers = [8, 12, 16, 20, 24, 28, 32, 40, 48]; - -export const panelRegistry = { - guildBar: { - id: "guildBar", - classId: "guild-bar", - label: "Guild Bar", - collapsedKey: "guildBarCollapsed", - }, - channelList: { - id: "channelList", - classId: "channel-list", - label: "Channel List", - collapsedKey: "channelListCollapsed", - }, - membersList: { - id: "membersList", - classId: "members-list", - label: "Members List", - collapsedKey: "membersListCollapsed", - }, - chatButtons: { - id: "chatButtons", - classId: "chat-buttons", - label: "Message Buttons", - collapsedKey: "chatButtonsCollapsed", - }, - titleBar: { - id: "titleBar", - classId: "title-bar", - label: "Title Bar", - collapsedKey: "titleBarCollapsed", - }, - headerBar: { - id: "headerBar", - classId: "header-bar", - label: "Header Bar", - collapsedKey: "headerBarCollapsed", - }, - userArea: { - id: "userArea", - classId: "user-area", - label: "User Area", - collapsedKey: "userAreaCollapsed", - }, -} as const; - -export type PanelId = keyof typeof panelRegistry; - -export const toolbarPanelOrder = ["guildBar", "channelList", "membersList", "chatButtons", "titleBar", "headerBar", "userArea"] as const satisfies readonly PanelId[]; - -export const settings = definePluginSettings({ - collapsedSize: { - type: OptionType.SLIDER, - description: "Collapsed hover area size in pixels.", - default: 12, - markers: collapsedSizeMarkers, - }, - transitionMs: { - type: OptionType.SLIDER, - description: "Panel transition speed in milliseconds.", - default: 160, - markers: transitionMarkers, - }, - guildBarCollapsed: { - type: OptionType.BOOLEAN, - description: "Persist the guild bar as collapsed.", - default: false, - }, - channelListCollapsed: { - type: OptionType.BOOLEAN, - description: "Persist the channel list as collapsed.", - default: false, - }, - membersListCollapsed: { - type: OptionType.BOOLEAN, - description: "Persist the members list as collapsed.", - default: false, - }, - chatButtonsCollapsed: { - type: OptionType.BOOLEAN, - description: "Persist the message button row as collapsed.", - default: false, - }, - titleBarCollapsed: { - type: OptionType.BOOLEAN, - description: "Persist the title bar as collapsed.", - default: false, - }, - headerBarCollapsed: { - type: OptionType.BOOLEAN, - description: "Persist the header bar as collapsed.", - default: false, - }, - userAreaCollapsed: { - type: OptionType.BOOLEAN, - description: "Persist the user area as collapsed.", - default: false, - }, -}); diff --git a/src/equicordplugins/collapsibleUi/style.css b/src/equicordplugins/collapsibleUi/style.css deleted file mode 100644 index c2a0b619f..000000000 --- a/src/equicordplugins/collapsibleUi/style.css +++ /dev/null @@ -1,416 +0,0 @@ -@property --vc-cui-collapsed-size { - syntax: ""; - inherits: true; - initial-value: 12px; -} - -@property --vc-cui-transition { - syntax: "