Merge branch 'dev' into translation

This commit is contained in:
thororen1234 2026-05-20 09:40:44 -04:00
commit aec3c153a5
No known key found for this signature in database
239 changed files with 7448 additions and 5885 deletions

33
.github/workflows/autofix.yml vendored Normal file
View file

@ -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

View file

@ -73,7 +73,7 @@ jobs:
gh release upload latest --clobber dist/release/* gh release upload latest --clobber dist/release/*
- name: Upload Plugin JSONs to Equibored repo - name: Upload Plugin JSONs to Equibored repo
if: github.repository == 'Equicord/Equicord' && !vars.ACT == 'false' if: github.repository == 'Equicord/Equicord' && vars.ACT == 'false'
run: | run: |
git config --global user.name "GitHub-Actions" git config --global user.name "GitHub-Actions"
git config --global user.email actions@github.com git config --global user.email actions@github.com

View file

@ -83,7 +83,7 @@ jobs:
".git/*" ".git/*"
- name: Publish extension - 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 run: pnpx @equicord/publish-browser-extension@latest --chrome-zip
dist/extension-chrome.zip --firefox-zip dist/extension-chrome.zip --firefox-zip
dist/extension-firefox.zip --firefox-sources-zip dist/extension-firefox.zip --firefox-sources-zip

345
.rules
View file

@ -1,213 +1,238 @@
# Equicord Development Guidelines # Equicord Development Rules
## Critical Rules ## Philosophy
- **NEVER** edit existing license headers. New files use 2026. - Solve the problem at hand. No refactors unless asked.
- Push back when the user idea is wrong or not correct. - Smallest diff that fixes it. Three similar lines beats a premature helper.
- **NEVER** git checkout files without explicit permission. - Less code with the same behavior wins.
- Always verify components/functions arent deprecated before using. - Never add comments unless asked. Keep existing comments, they carry context.
- **NEVER** use `any` for Discord objects. Import proper types from `@vencord/discord-types`. - Delete dead code. Don't comment it out.
- Fix with smallest possible change. No refactoring unless explicitly asked. - Natural human text in errors, descriptions, toasts. No dashes, no robotic phrasing. Descriptions start capitalized and end with a period.
- 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.
## TypeScript ## TypeScript
- **Prefer:** optional chaining (`?.`), nullish coalescing (`??`), `const`, arrow functions - `strict: true`, `noImplicitAny: false`, `noUncheckedIndexedAccess: false`. Annotate params explicitly. Array index access is not auto `undefined`.
- **Use:** destructuring, template literals, object shorthand, array methods - Never `any`. Use `unknown` and narrow, or type webpack results via the `find*` generic.
- **Style:** early returns, trust inference, inline single-use variables - 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 })`. What does NOT get merged:
- Return `null` for conditional rendering, not `undefined`.
- **Forbidden:** `React.cloneElement`, `React.isValidElement`, `React.memo()`, `React.lazy`, `React.Children`
- Always return cleanup functions in `useEffect`.
## 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. - Folder per plugin. `src/plugins/<name>/index.tsx` or `src/equicordplugins/<name>/index.tsx`. Single file plugins still get a folder.
- Empty catch blocks. - Folder suffixes the build system filters on: `.desktop`, `.web`, `.dev`, `.discordDesktop`, `.vesktop`, `.equibop`.
- Hardcoded minified vars in patches (`e,t,n`). - `export default definePlugin({ ... })`. Never named.
- **NEVER** use `settings.use()` with arrays. Mutate then reassign. - 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. Use these plugin object fields instead of calling `add*` or `remove*` manually. `PluginManager` handles lifecycle and auto enables the matching API plugin.
- 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.
## 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` Wrapper style fields take `{ render | wrapper, priority }`, never a bare function.
- `array && array.length > 0` ⟹ `array.length` (only when array is guaranteed to exist)
- `settings?.store?.value` ⟹ `settings.store.value` 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.
- `value || defaultValue` ⟹ `value ?? defaultValue`
### 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<T>()` 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 ## 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. ### Examples
- 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:**
```js ```js
// clean replacement
find: "#{intl::PIN_MESSAGE}),icon:" find: "#{intl::PIN_MESSAGE}),icon:"
match: /#{intl::PIN_MESSAGE}\)/ match: /#{intl::PIN_MESSAGE}\)/
replace: "$self.getPinLabel(arguments[0]))" replace: "$self.getPinLabel(arguments[0]))"
```
**Appending:** // appending
```js
find: "#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}" find: "#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}"
match: /#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}.{0,100}(?=])/ match: /#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}.{0,100}(?=])/
replace: "$&,$self.renderTooltip(arguments[0].guild)" replace: "$&,$self.renderTooltip(arguments[0].guild)"
```
**Wrapping:** // wrapping with lookbehind
```js
find: "#{intl::SEVERAL_USERS_TYPING_STRONG}" find: "#{intl::SEVERAL_USERS_TYPING_STRONG}"
match: /(?<="aria-atomic":!0,children:)\i/ match: /(?<="aria-atomic":!0,children:)\i/
replace: "$self.renderTypingUsers({ children: $& })" replace: "$self.renderTypingUsers({ children: $& })"
``` ```
## Plugin Interop ## Webpack access
- `isPluginEnabled(name)` from `@api/PluginManager` takes a string, checks required/isDependency/enabled. - Top level: lazy finders only. `findByPropsLazy`, `findByCodeLazy`, `findComponentByCodeLazy`, `findStoreLazy`, `findCssClassesLazy`, `getUserSettingLazy`, `proxyLazyWebpack`.
- Import the plugin directly from its path to access `.name` and functions. - Direct `find(...)` only inside `start()` or later runtime callbacks.
- Dont use `Vencord.Plugins.plugins` or `plugin.started`. Dont use `"as unknown as"` casting. - 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 ## React
import { isPluginEnabled } from "@api/PluginManager";
import otherPlugin from "@equicordplugins/otherPlugin"; - Import hooks and `React` from `@webpack/common`. Never `import React from "react"`. Only `import type` from `"react"` (`ReactNode`, `ComponentType`, etc.).
if (!isPluginEnabled(otherPlugin.name)) return null; - Forbidden: `React.FC`, `React.memo`, `React.lazy`, `React.Children`, `React.cloneElement`, `React.isValidElement`. Type props with a named `interface`.
otherPlugin.someFunction(); - 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-<plugin>-`. 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-<plugin>-`. 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<typeof import("./native")>;
```
- `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 ## 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` React and hooks. `React`, `useState`, `useEffect`, `useLayoutEffect`, `useMemo`, `useRef`, `useReducer`, `useCallback`, `useStateFromStores`.
- **@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`
### 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?)` Action creators. `MessageActions`, `ChannelActionCreators`, `UserProfileActions`, `InviteActions`, `GuildActions`, `VoiceActions`, `DraftActions`, `PinActions`, `MessageCache`.
- `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)`
#### 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` ### `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`.
#### 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`

View file

@ -21,7 +21,8 @@ Windows
MacOS 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 Linux

View file

@ -55,6 +55,7 @@ window.VencordNative = {
native: { native: {
getVersions: () => ({}), getVersions: () => ({}),
supportsWindowsMaterial: () => false,
openExternal: async (url) => void open(url, "_blank"), openExternal: async (url) => void open(url, "_blank"),
getRendererCss: async () => { getRendererCss: async () => {
if (IS_USERSCRIPT) if (IS_USERSCRIPT)

View file

@ -1,7 +1,7 @@
{ {
"name": "equicord", "name": "equicord",
"private": "true", "private": "true",
"version": "1.14.12.0", "version": "1.14.13.0",
"description": "The other cutest Discord client mod", "description": "The other cutest Discord client mod",
"homepage": "https://github.com/Equicord/Equicord#readme", "homepage": "https://github.com/Equicord/Equicord#readme",
"bugs": { "bugs": {
@ -33,8 +33,11 @@
"repair": "node scripts/runInstaller.mjs -- --repair", "repair": "node scripts/runInstaller.mjs -- --repair",
"lint": "eslint", "lint": "eslint",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins", "lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint-styles:fix": "pnpm lint-styles --fix",
"lint:fix": "pnpm lint --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", "testWeb": "pnpm lint:fix && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit", "testTsc": "tsc --noEmit",
"depCheck": "node scripts/depCheck.mjs" "depCheck": "node scripts/depCheck.mjs"
@ -98,31 +101,7 @@
"typescript-transform-paths": "^3.5.6", "typescript-transform-paths": "^3.5.6",
"zip-local": "^0.3.5" "zip-local": "^0.3.5"
}, },
"packageManager": "pnpm@10.33.0", "packageManager": "pnpm@11.0.9",
"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"
}
},
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }

View file

@ -192,6 +192,7 @@ export type TextInput = ComponentType<PropsWithChildren<{
export type TextArea = ComponentType<Omit<HTMLProps<HTMLTextAreaElement>, "onChange"> & { export type TextArea = ComponentType<Omit<HTMLProps<HTMLTextAreaElement>, "onChange"> & {
onChange(v: string): void; onChange(v: string): void;
inputRef?: Ref<HTMLTextAreaElement>; inputRef?: Ref<HTMLTextAreaElement>;
autosize?: boolean;
}>; }>;
export interface SelectOption { export interface SelectOption {

View file

@ -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<HTMLDivElement>;
/** 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<ModalProps>;
export type ConfirmModal = ComponentType<ConfirmModalProps>;
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<RenderModal>, options?: ModalOptions & { contextKey?: string; }) => Promise<string>;
/**
* 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;
}

View file

@ -1,5 +1,7 @@
export * from "./common"; export * from "./common";
// TODO: Splitting up components into files in the folder would be good
export * from "./components"; export * from "./components";
export * from "./components/Modal";
export * from "./flux"; export * from "./flux";
export * from "./fluxEvents"; export * from "./fluxEvents";
export * from "./menu"; export * from "./menu";

View file

@ -1,2 +1,23 @@
packages: packages:
- 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: "*"

54
scripts/lintIntl.mjs Normal file
View file

@ -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);

70
scripts/lintPatches.mjs Normal file
View file

@ -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);

View file

@ -38,7 +38,14 @@ function getFilename() {
case "win32": case "win32":
return "EquilotlCli.exe"; return "EquilotlCli.exe";
case "darwin": 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": case "linux":
return "EquilotlCli-linux"; return "EquilotlCli-linux";
default: default:

View file

@ -18,6 +18,7 @@
// DO NOT REMOVE UNLESS YOU WISH TO FACE THE WRATH OF THE CIRCULAR DEPENDENCY DEMON!!!!!!! // DO NOT REMOVE UNLESS YOU WISH TO FACE THE WRATH OF THE CIRCULAR DEPENDENCY DEMON!!!!!!!
import "~plugins"; import "~plugins";
import "./fixWeirdAppRegionBug.css";
export * as Api from "./api"; export * as Api from "./api";
export * as Plugins from "./api/PluginManager"; export * as Plugins from "./api/PluginManager";

View file

@ -77,6 +77,7 @@ export default {
native: { native: {
getVersions: () => process.versions as Partial<NodeJS.ProcessVersions>, getVersions: () => process.versions as Partial<NodeJS.ProcessVersions>,
supportsWindowsMaterial: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_MATERIAL),
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url), openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url),
getRendererCss: () => invoke<string>(IpcEvents.GET_RENDERER_CSS), getRendererCss: () => invoke<string>(IpcEvents.GET_RENDERER_CSS),
onRendererCssUpdate: (cb: (newCss: string) => void) => { onRendererCssUpdate: (cb: (newCss: string) => void) => {

View file

@ -138,7 +138,7 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
return ( return (
<Tooltip text={props.tooltip}> <Tooltip text={props.tooltip}>
{({ onMouseEnter, onMouseLeave }) => ( {({ onMouseEnter, onMouseLeave }) => (
<div className={`expression-picker-chat-input-button ${ChannelTextAreaClasses?.buttonContainer ?? ""}`}> <div className={`expression-picker-chat-input-button ${ChannelTextAreaClasses?.buttonContainer ?? ""} vc-chatbar-button`}>
<Clickable <Clickable
aria-label={props.tooltip} aria-label={props.tooltip}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
@ -191,20 +191,28 @@ addContextMenuPatch("textarea-context", (children, args) => {
); );
}); });
// Wrapper stuff is down here to avoid conflicts with above
export type ChatBarButtonWrapper = (buttons: ReactNode) => ReactNode; export type ChatBarButtonWrapper = (buttons: ReactNode) => ReactNode;
/** export interface ChatBarButtonWrapperData {
* Registry for plugins that need to wrap the entire chat bar button container wrapper: ChatBarButtonWrapper;
*/ priority: number;
export const ChatBarButtonWrappers = new Map<string, ChatBarButtonWrapper>(); }
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<string, ChatBarButtonWrapperData>();
export const addChatBarButtonWrapper = (id: string, wrapper: ChatBarButtonWrapper, priority: number = 0) => ChatBarButtonWrappers.set(id, { wrapper, priority });
export const removeChatBarButtonWrapper = (id: string) => ChatBarButtonWrappers.delete(id); export const removeChatBarButtonWrapper = (id: string) => ChatBarButtonWrappers.delete(id);
export function _wrapButtons(buttons: ReactNode) { export function _wrapButtons(buttons: ReactNode) {
const sorted = [...ChatBarButtonWrappers.values()]
.sort((a, b) => a.priority - b.priority);
let wrapped = buttons; let wrapped = buttons;
for (const wrapper of ChatBarButtonWrappers.values()) { for (const { wrapper } of sorted) {
wrapped = wrapper(wrapped); wrapped = wrapper(wrapped);
} }
return wrapped; return wrapped;

View file

@ -18,15 +18,13 @@
import * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { BaseText } from "@components/BaseText";
import { Flex } from "@components/Flex";
import { Paragraph } from "@components/Paragraph"; import { Paragraph } from "@components/Paragraph";
import { openNotificationSettingsModal } from "@components/settings/tabs/vencord/NotificationSettings"; import { openNotificationSettingsModal } from "@components/settings/tabs/vencord/NotificationSettings";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { closeModal, ModalCloseButton, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { t } from "@utils/translation"; 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 { nanoid } from "nanoid";
import type { DispatchWithoutAction } from "react"; import type { DispatchWithoutAction } from "react";
@ -133,7 +131,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) { export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {
if (!log.length && !pending) if (!log.length && !pending)
return ( return (
<div className={cl("container")}> <div>
<div className={cl("empty")} /> <div className={cl("empty")} />
<Paragraph style={{ textAlign: "center" }}> <Paragraph style={{ textAlign: "center" }}>
{t("api.notifications.noNotifications")} {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(); const [log, pending] = useLogs();
return ( return (
<ModalRoot {...modalProps} size={ModalSize.LARGE} className={cl("modal")}> <Modal
<ModalHeader> {...props}
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>{t("api.notifications.notificationLog")}</BaseText> size="xl"
<ModalCloseButton onClick={close} /> title={t("api.notifications.notificationLog")}
</ModalHeader> actions={[
{
<div style={{ width: "100%" }}> text: t("api.notifications.notificationSettings"),
<NotificationLog log={log} pending={pending} /> variant: "primary",
</div> onClick: openNotificationSettingsModal
},
<ModalFooter> {
<Flex> text: t("api.notifications.logs.clearNotificationLog"),
<Button onClick={openNotificationSettingsModal}> variant: "critical-primary",
{t("api.notifications.notificationSettings")} disabled: !log.length,
</Button> onClick() {
openModal(props =>
<Button <ConfirmModal
disabled={log.length === 0} {...props}
color={Button.Colors.RED} title={t("api.notifications.logs.title")}
onClick={() => { subtitle={t("api.notifications.logs.body", { count: log.length })}
Alerts.show({ confirmText={t("api.notifications.logs.confirm")}
title: t("api.notifications.logs.title"), cancelText={t("api.notifications.logs.cancel")}
body: t("api.notifications.logs.body", { count: log.length }), onConfirm={async () => {
async onConfirm() {
await DataStore.set(KEY, []); await DataStore.set(KEY, []);
signals.forEach(x => x()); signals.forEach(x => x());
}, }}
confirmText: t("api.notifications.logs.confirm"), />
confirmColor: "vc-notification-log-danger-btn", );
cancelText: t("api.notifications.cancel") }
}); }
}} ]}
> >
{t("api.notifications.clearNotificationLog")} <NotificationLog log={log} pending={pending} />
</Button> </Modal>
</Flex>
</ModalFooter>
</ModalRoot>
); );
} }
export function openNotificationLogModal() { export function openNotificationLogModal() {
const key = openModal(modalProps => ( openModal(props => <LogModal {...props} />);
<LogModal
modalProps={modalProps}
close={() => closeModal(key)}
/>
));
} }

View file

@ -89,12 +89,6 @@
margin-bottom: 40px; margin-bottom: 40px;
} }
.vc-notification-log-container {
padding: 1em;
max-height: min(750px, 75vh);
width: 100%;
}
.vc-notification-log-wrapper { .vc-notification-log-wrapper {
height: 120px; height: 120px;
width: 100%; width: 100%;

View file

@ -288,9 +288,9 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
} }
if (audioProcessor) addAudioProcessor(name, audioProcessor); if (audioProcessor) addAudioProcessor(name, audioProcessor);
if (userAreaButton) addUserAreaButton(name, userAreaButton.render, userAreaButton.priority); if (userAreaButton) addUserAreaButton(name, userAreaButton.render, userAreaButton.priority);
if (renderProfileCollection) addProfileCollection(name, renderProfileCollection); if (renderProfileCollection) addProfileCollection(name, renderProfileCollection.render, renderProfileCollection.priority);
if (chatBarButtonWrapper) addChatBarButtonWrapper(name, chatBarButtonWrapper); if (chatBarButtonWrapper) addChatBarButtonWrapper(name, chatBarButtonWrapper.wrapper, chatBarButtonWrapper.priority);
if (renderProfileSection) addProfileSection(name, renderProfileSection); if (renderProfileSection) addProfileSection(name, renderProfileSection.render, renderProfileSection.priority);
return true; return true;
}, p => `startPlugin ${p.name}`); }, p => `startPlugin ${p.name}`);
@ -382,7 +382,7 @@ export const initPluginManager = onlyOnce(function init() {
"onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick", "onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick",
"renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration", "renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration",
// Custom // Custom
"renderNicknameIcon", "renderProfileCollection" "renderNicknameIcon"
]; ];
const neededApiPlugins = new Set<string>(); const neededApiPlugins = new Set<string>();
@ -429,7 +429,7 @@ export const initPluginManager = onlyOnce(function init() {
if (p.renderProfileSection) neededApiPlugins.add("ProfileSectionsAPI"); if (p.renderProfileSection) neededApiPlugins.add("ProfileSectionsAPI");
for (const key of pluginKeysToBind) { for (const key of pluginKeysToBind) {
p[key] &&= p[key].bind(p) as any; p[key] &&= (p[key] as Function).bind(p) as any;
} }
} }

View file

@ -14,11 +14,12 @@ const logger = new Logger("ProfileCollectionAPI");
export type ProfileCollectionFactory = (props: { user: User; isSideBar: boolean; }) => ReactNode | Promise<ReactNode> | null; export type ProfileCollectionFactory = (props: { user: User; isSideBar: boolean; }) => ReactNode | Promise<ReactNode> | null;
interface CollectionEntry { export interface ProfileCollectionData {
render: ProfileCollectionFactory; render: ProfileCollectionFactory;
priority: number;
} }
const profileCollections = new Map<string, CollectionEntry>(); const profileCollections = new Map<string, ProfileCollectionData>();
const profileCollectionListeners = new Set<() => void>(); const profileCollectionListeners = new Set<() => void>();
/** /**
@ -34,7 +35,7 @@ const profileCollectionListeners = new Set<() => void>();
* )); * ));
*/ */
export function addProfileCollection(id: string, render: ProfileCollectionFactory, priority = 0) { export function addProfileCollection(id: string, render: ProfileCollectionFactory, priority = 0) {
profileCollections.set(id, { render }); profileCollections.set(id, { render, priority });
profileCollectionListeners.forEach(listener => listener()); profileCollectionListeners.forEach(listener => listener());
} }
@ -58,6 +59,7 @@ function ProfileCollections({ props }: { props: any; }) {
}, []); }, []);
return Array.from(profileCollections) return Array.from(profileCollections)
.sort(([, a], [, b]) => b.priority - a.priority)
.map(([id, { render: Collection }]) => ( .map(([id, { render: Collection }]) => (
<ErrorBoundary noop key={id} onError={e => logger.error(`Failed to render profile collection: ${id}`, e.error)}> <ErrorBoundary noop key={id} onError={e => logger.error(`Failed to render profile collection: ${id}`, e.error)}>
<Collection {...props} user={props.user ?? props.currentUser} /> <Collection {...props} user={props.user ?? props.currentUser} />

View file

@ -13,12 +13,12 @@ const logger = new Logger("ProfileSectionsAPI");
export type ProfileSectionFactory = (props: { userId: string; isSideBar: boolean; }) => ReactNode | Promise<ReactNode> | null; export type ProfileSectionFactory = (props: { userId: string; isSideBar: boolean; }) => ReactNode | Promise<ReactNode> | null;
interface SectionEntry { export interface ProfileSectionData {
render: ProfileSectionFactory; render: ProfileSectionFactory;
priority: number; priority: number;
} }
const profileSections = new Map<string, SectionEntry>(); const profileSections = new Map<string, ProfileSectionData>();
const profileSectionListeners = new Set<() => void>(); const profileSectionListeners = new Set<() => void>();
/** /**

View file

@ -70,6 +70,7 @@ export interface Settings {
| "under-page" | "under-page"
| "window" | "window"
| undefined; | undefined;
windowsMaterial: "none" | "mica" | "tabbed" | "acrylic";
disableMinSize: boolean; disableMinSize: boolean;
winNativeTitleBar: boolean; winNativeTitleBar: boolean;
plugins: { plugins: {
@ -126,6 +127,7 @@ const DefaultSettings: Settings = {
transparent: false, transparent: false,
winCtrlQ: false, winCtrlQ: false,
macosVibrancyStyle: undefined, macosVibrancyStyle: undefined,
windowsMaterial: "none",
disableMinSize: false, disableMinSize: false,
winNativeTitleBar: false, winNativeTitleBar: false,
plugins: {}, plugins: {},

View file

@ -8,9 +8,8 @@ import * as DataStore from "@api/DataStore";
import { showNotification } from "@api/Notifications"; import { showNotification } from "@api/Notifications";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { t } from "@utils/translation"; 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"); export const logger = new Logger("SettingsSync:CloudSetup", "#39b7e0");

View file

@ -13,6 +13,7 @@
letter-spacing: 0.02em; letter-spacing: 0.02em;
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap;
} }
.vc-badge-icon { .vc-badge-icon {
@ -51,4 +52,4 @@
.vc-badge-premium { .vc-badge-premium {
background-color: var(--premium-tier-2-purple-for-gradients); background-color: var(--premium-tier-2-purple-for-gradients);
color: var(--white); color: var(--white);
} }

View file

@ -11,6 +11,10 @@
color: var(--text-default); color: var(--text-default);
} }
.vc-card-normal {
border: 1px solid var(--border-subtle);
}
.vc-card-danger { .vc-card-danger {
background-color: var(--background-feedback-critical); background-color: var(--background-feedback-critical);
border-color: var(--background-feedback-critical); border-color: var(--background-feedback-critical);
@ -56,4 +60,4 @@
.vc-card-defaultPadding { .vc-card-defaultPadding {
padding: 1em; padding: 1em;
} }

View file

@ -37,7 +37,6 @@ import { classNameFactory } from "@utils/css";
import { copyWithToast, openInviteModal } from "@utils/discord"; import { copyWithToast, openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { openModal } from "@utils/modal";
import { relaunch, showItemInFolder } from "@utils/native"; import { relaunch, showItemInFolder } from "@utils/native";
import { useAwaiter, useForceUpdater } from "@utils/react"; import { useAwaiter, useForceUpdater } from "@utils/react";
import type { ThemeHeader } from "@utils/themes"; import type { ThemeHeader } from "@utils/themes";
@ -46,7 +45,7 @@ import { usercssParse } from "@utils/themes/usercss";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { getStylusWebStoreUrl } from "@utils/web"; import { getStylusWebStoreUrl } from "@utils/web";
import { findComponentByCodeLazy, findLazy } from "@webpack"; 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 { ContextMenuApi } from "@webpack/common/menu";
import type { ComponentType, Ref, SyntheticEvent } from "react"; import type { ComponentType, Ref, SyntheticEvent } from "react";
import type { UserstyleHeader } from "usercss-meta"; import type { UserstyleHeader } from "usercss-meta";

View file

@ -9,15 +9,15 @@ import { BaseText } from "@components/BaseText";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { CopyIcon, PasteIcon, ResetIcon } from "@components/Icons"; import { CopyIcon, PasteIcon, ResetIcon } from "@components/Icons";
import { copyWithToast } from "@utils/discord"; import { copyWithToast } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; import { RenderModalProps } from "@vencord/discord-types";
import { showToast, Toasts, Tooltip } from "@webpack/common"; import { Modal, showToast, Toasts, Tooltip } from "@webpack/common";
import { type ReactNode } from "react"; import { type ReactNode } from "react";
import { UserstyleHeader } from "usercss-meta"; import { UserstyleHeader } from "usercss-meta";
import { SettingBooleanComponent, SettingColorComponent, SettingNumberComponent, SettingRangeComponent, SettingSelectComponent, SettingTextComponent } from "./components"; import { SettingBooleanComponent, SettingColorComponent, SettingNumberComponent, SettingRangeComponent, SettingSelectComponent, SettingTextComponent } from "./components";
interface UserCSSSettingsModalProps { interface UserCSSSettingsModalProps {
modalProps: ModalProps; modalProps: RenderModalProps;
theme: UserstyleHeader; theme: UserstyleHeader;
onSettingsReset: () => void; onSettingsReset: () => void;
} }
@ -186,21 +186,22 @@ export function UserCSSSettingsModal({ modalProps, theme, onSettingsReset }: Use
} }
return ( return (
<ModalRoot {...modalProps}> <Modal
<ModalHeader separator={false}> {...modalProps}
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>Settings for {theme.name}</BaseText> title={
<Flex style={{ gap: 4, marginRight: 4 }} className="vc-settings-usercss-ie-buttons"> <Flex style={{ alignItems: "center", gap: 4 }}>
<ExportButton themeSettings={themeVars} /> <BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>Settings for {theme.name}</BaseText>
<ImportButton themeSettings={themeVars} /> <Flex style={{ gap: 4, marginRight: 4 }} className="vc-settings-usercss-ie-buttons">
<ResetButton themeSettings={userCssVars} themeId={theme.id} close={modalProps.onClose} onReset={onSettingsReset} /> <ExportButton themeSettings={themeVars} />
<ImportButton themeSettings={themeVars} />
<ResetButton themeSettings={userCssVars} themeId={theme.id} close={modalProps.onClose} onReset={onSettingsReset} />
</Flex>
</Flex> </Flex>
<ModalCloseButton onClick={modalProps.onClose} /> }
</ModalHeader> >
<ModalContent> <Flex flexDirection="column" style={{ gap: 12, marginBottom: 16 }}>
<Flex flexDirection="column" style={{ gap: 12, marginBottom: 16 }}> {controls}
{controls} </Flex>
</Flex> </Modal>
</ModalContent>
</ModalRoot>
); );
} }

View file

@ -32,7 +32,7 @@
} }
.vc-addon-note { .vc-addon-note {
height: 36px; height: 34px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
display: -webkit-box; display: -webkit-box;
@ -105,4 +105,4 @@
.vc-addon-footer { .vc-addon-footer {
font-size: small; font-size: small;
} }

View file

@ -16,12 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { BaseText } from "@components/BaseText";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed"; import { handleComponentFailed } from "@components/handleComponentFailed";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { onlyOnce } from "@utils/onlyOnce"; import { onlyOnce } from "@utils/onlyOnce";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { Modal, openModal } from "@webpack/common";
import type { ComponentType, PropsWithChildren } from "react"; import type { ComponentType, PropsWithChildren } from "react";
export function SettingsTab({ children }: PropsWithChildren) { export function SettingsTab({ children }: PropsWithChildren) {
@ -43,19 +42,18 @@ export function wrapTab(component: ComponentType<any>, tab: string) {
} }
export function openSettingsTabModal(Tab: ComponentType<any>) { export function openSettingsTabModal(Tab: ComponentType<any>) {
try { Tab = wrapTab(Tab, Tab.displayName || "SettingsTab");
openModal(wrapTab((modalProps: ModalProps) => (
<ModalRoot {...modalProps} size={ModalSize.MEDIUM} className="vc-settings-modal-root">
<ModalHeader>
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>{Tab.displayName?.replace("SettingsTab", "") || "Settings"}</BaseText>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
<ModalContent className="vc-settings-modal"> try {
<Tab /> openModal(props => (
</ModalContent> <Modal
</ModalRoot> {...props}
), Tab.displayName || "SettingsTab")); size="lg"
title={Tab.displayName?.replace("SettingsTab", "") || "Settings"}
>
<Tab />
</Modal>
));
} catch { } catch {
handleSettingsTabError(); handleSettingsTabError();
} }

View file

@ -13,8 +13,8 @@ import { ReplaceFn } from "@utils/types";
import { Button, Parser, useMemo, useState } from "@webpack/common"; import { Button, Parser, useMemo, useState } from "@webpack/common";
import type { Change } from "diff"; import type { Change } from "diff";
// Do not include diff in non dev builds (side effects import) // Do not include diff in standalone builds (side effects import)
if (IS_DEV) { if (!IS_STANDALONE) {
var differ = require("diff") as typeof import("diff"); var differ = require("diff") as typeof import("diff");
} }
@ -130,7 +130,10 @@ export function PatchPreview({ module, match, replacement, setReplacementError }
className={Margins.top20} className={Margins.top20}
onClick={() => { onClick={() => {
try { 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"]); setCompileResult([true, "Compiled successfully"]);
} catch (err) { } catch (err) {
setCompileResult([false, (err as Error).message]); setCompileResult([false, (err as Error).message]);

View file

@ -183,4 +183,4 @@ function PatchHelper() {
); );
} }
export default IS_DEV ? wrapTab(PatchHelper, "Patch Helper") : null; export default !IS_STANDALONE ? wrapTab(PatchHelper, "PatchHelper") : null;

View file

@ -1,12 +1,12 @@
.vc-author-modal-root { .vc-author-modal-root {
padding: 1em; padding: 0 0 16px;
} }
.vc-author-modal-header { .vc-author-modal-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 1em; margin-bottom: 4px;
} }
.vc-author-modal-name { .vc-author-modal-name {
@ -50,18 +50,16 @@
0 2px 6px rgb(0 0 0 / 35%); 0 2px 6px rgb(0 0 0 / 35%);
} }
.vc-author-modal-root+[class*="footer"] {
padding: 16px;
}
.vc-author-modal-links { .vc-author-modal-links {
display: flex; display: flex;
gap: 4px; gap: 4px;
margin-left: auto; margin-left: auto;
width: 100%;
justify-content: flex-end;
} }
.vc-author-modal-plugins { .vc-author-modal-plugins {
display: grid; display: grid;
gap: 0.5em; gap: 0.5em;
margin-top: 0.75em; margin-top: 0.75em;
} }

View file

@ -7,17 +7,15 @@
import "./ContributorModal.css"; import "./ContributorModal.css";
import { useSettings } from "@api/Settings"; import { useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import { Heading } from "@components/Heading";
import { HeadingPrimary } from "@components/Heading";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Paragraph } from "@components/Paragraph"; import { Paragraph } from "@components/Paragraph";
import { EquicordDevsById, VencordDevsById } from "@utils/constants"; import { EquicordDevsById, VencordDevsById } from "@utils/constants";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { fetchUserProfile } from "@utils/discord"; import { fetchUserProfile } from "@utils/discord";
import { ModalContent, ModalFooter, ModalRoot, openModal } from "@utils/modal";
import { Translate } from "@utils/translation"; import { Translate } from "@utils/translation";
import { User } from "@vencord/discord-types"; import { RenderModalProps, User } from "@vencord/discord-types";
import { showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common"; import { Modal, openModal, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import Plugins, { PluginMeta } from "~plugins"; import Plugins, { PluginMeta } from "~plugins";
@ -27,16 +25,10 @@ import { PluginCard } from "./PluginCard";
const cl = classNameFactory("vc-author-modal-"); const cl = classNameFactory("vc-author-modal-");
export function openContributorModal(user: User) { export function openContributorModal(user: User) {
openModal(modalProps => openModal(modalProps => <ContributorModal user={user} modalProps={modalProps} />);
<ModalRoot {...modalProps}>
<ErrorBoundary>
<ContributorModal user={user} />
</ErrorBoundary>
</ModalRoot>
);
} }
function ContributorModal({ user }: { user: User; }) { function ContributorModal({ user, modalProps }: { user: User; modalProps: RenderModalProps; }) {
useSettings(); useSettings();
const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id)); const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id));
@ -68,18 +60,20 @@ function ContributorModal({ user }: { user: User; }) {
const hasLinks = website || githubName; const hasLinks = website || githubName;
return ( return (
<> <Modal
<ModalContent className={cl("root")}> {...modalProps}
title={
<div className={cl("header")}> <div className={cl("header")}>
<img <img
className={cl("avatar")} className={cl("avatar")}
src={user.getAvatarURL(void 0, 512, true)} src={user.getAvatarURL(void 0, 512, true)}
alt="" alt=""
/> />
<HeadingPrimary className={cl("name")}>{user.username}</HeadingPrimary> <Heading tag="h2" className={cl("name")}>{user.username}</Heading>
</div> </div>
}
{plugins.length ? ( subtitle={
plugins.length ? (
<Paragraph> <Paragraph>
<Translate <Translate
i18nKey="vencord.settings.plugins.contributorModal.contributorInfo.plugins" i18nKey="vencord.settings.plugins.contributorModal.contributorInfo.plugins"
@ -94,25 +88,14 @@ function ContributorModal({ user }: { user: User; }) {
to Equicord in other ways! to Equicord in other ways!
</Translate> </Translate>
</Paragraph > </Paragraph >
)} )
}
{!!plugins.length && ( actionBarInput={
<div className={cl("plugins")}> hasLinks && (
{plugins.map(p => <div
<PluginCard className={cl("links")}
key={p.name} style={{ width: "100%", justifyContent: "flex-end" }}
plugin={p} >
disabled={p.required ?? false}
onRestartNeeded={() => showToast("Restart to apply changes!")}
/>
)}
</div>
)}
</ModalContent>
{hasLinks && (
<ModalFooter>
<div className={cl("links")}>
{website && ( {website && (
<WebsiteButton <WebsiteButton
text={website} text={website}
@ -126,8 +109,23 @@ function ContributorModal({ user }: { user: User; }) {
/> />
)} )}
</div> </div>
</ModalFooter> )
)} }
</> >
<div className={cl("root")}>
{!!plugins.length && (
<div className={cl("plugins")}>
{plugins.map(p =>
<PluginCard
key={p.name}
plugin={p}
disabled={p.required ?? false}
onRestartNeeded={() => showToast("Restart to apply changes!")}
/>
)}
</div>
)}
</div>
</Modal>
); );
} }

View file

@ -1,7 +1,6 @@
.vc-plugin-modal-header { .vc-plugin-modal-header {
align-items: flex-start; align-items: flex-start;
position: relative; position: relative;
padding: 16px 16px 0 24px;
} }
.vc-plugin-modal-header-content { .vc-plugin-modal-header-content {
@ -70,7 +69,7 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
margin-block: 8px 16px; margin-top: 8px;
} }
.vc-plugin-modal-tag { .vc-plugin-modal-tag {
@ -82,3 +81,9 @@
padding: 6px 12px; padding: 6px 12px;
color: var(--text-default); color: var(--text-default);
} }
.vc-plugin-modal-about-box {
padding: 16px;
border-radius: 8px;
border: 1px solid var(--border-normal, var(--background-modifier-accent));
}

View file

@ -32,11 +32,10 @@ import { classNameFactory } from "@utils/css";
import { proxyLazy } from "@utils/lazy"; import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc"; 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 { 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 { 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 { Constructor } from "type-fest";
import { PluginMeta } from "~plugins"; import { PluginMeta } from "~plugins";
@ -53,7 +52,7 @@ const ConfirmModal = findComponentByCodeLazy('parentComponent:"ConfirmModal"');
const WarningIcon = findComponentByCodeLazy("3.15H3.29c-1.74"); const WarningIcon = findComponentByCodeLazy("3.15H3.29c-1.74");
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any; const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
interface PluginModalProps extends ModalProps { interface PluginModalProps extends RenderModalProps {
plugin: Plugin; plugin: Plugin;
onRestartNeeded(key: string): void; onRestartNeeded(key: string): void;
} }
@ -142,6 +141,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
onChange={debounce(onChange)} onChange={debounce(onChange)}
pluginSettings={pluginSettings} pluginSettings={pluginSettings}
definedSettings={settings} definedSettings={settings}
closePluginSettings={onClose}
/> />
</ErrorBoundary> </ErrorBoundary>
); );
@ -176,28 +176,36 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
const isEquicordPlugin = pluginMeta.folderName.startsWith("src/equicordplugins/") ?? false; const isEquicordPlugin = pluginMeta.folderName.startsWith("src/equicordplugins/") ?? false;
return ( return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}> <Modal
<ModalHeader separator={false} className={cl("header")}> transitionState={transitionState}
<div className={cl("header-content")}> onClose={onClose}
<BaseText size="lg" weight="semibold" className={cl("title")}>{plugin.name}</BaseText> size="lg"
<BaseText size="sm" className={cl("description")}>{plugin.description}</BaseText> title={
{!!plugin.tags?.length && <PluginTags tags={plugin.tags} />} <div className={cl("header")}>
{!!plugin.settingsAboutComponent && ( <BaseText tag="h1" weight="semibold" size="lg">{plugin.name}</BaseText>
<div className={Margins.top8}>
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
<plugin.settingsAboutComponent />
</ErrorBoundary>
</div>
)}
</div> </div>
<div className={cl("header-trailing")}> }
<CloseButton onClick={onClose} /> subtitle={
<div className={cl("info")}>
<div>
<Paragraph size="md">{plugin.description}</Paragraph>
{!!plugin.tags?.length && <PluginTags tags={plugin.tags} />}
</div>
</div> </div>
</ModalHeader> }
>
<ModalContent className={"vc-settings-modal-content"}> {!!plugin.settingsAboutComponent && (
<div className={classes(Margins.top16, cl("about-box"))}>
<section>
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
<plugin.settingsAboutComponent />
</ErrorBoundary>
</section>
</div>
)}
<div className={"vc-settings-modal-content"}>
<section> <section>
<BaseText size="lg" weight="semibold" color="text-strong" className={Margins.bottom8}>Authors</BaseText> <Text variant="heading-lg/semibold" className={classes(Margins.top8, Margins.bottom8)}>Authors</Text>
<div style={{ width: "fit-content" }}> <div style={{ width: "fit-content" }}>
<ErrorBoundary noop> <ErrorBoundary noop>
<UserSummaryItem <UserSummaryItem
@ -228,8 +236,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
<BaseText size="lg" weight="semibold" color="text-strong" className={classes(Margins.top16, Margins.bottom8)}>Settings</BaseText> <BaseText size="lg" weight="semibold" color="text-strong" className={classes(Margins.top16, Margins.bottom8)}>Settings</BaseText>
{renderSettings()} {renderSettings()}
</section> </section>
</ModalContent> </div>
<ModalFooter> <div>
<Flex flexDirection="column" style={{ width: "100%" }}> <Flex flexDirection="column" style={{ width: "100%" }}>
<Flex style={{ justifyContent: "space-between", alignItems: "center" }}> <Flex style={{ justifyContent: "space-between", alignItems: "center" }}>
{hasSettings ? ( {hasSettings ? (
@ -262,8 +270,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
)} )}
</Flex> </Flex>
</Flex> </Flex>
</ModalFooter> </div>
</ModalRoot > </Modal >
); );
} }

View file

@ -18,10 +18,9 @@
} }
.vc-plugin-ui-elements-modal-content { .vc-plugin-ui-elements-modal-content {
padding: 1em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5em; gap: 2em;
} }
.vc-plugin-ui-elements-switches { .vc-plugin-ui-elements-switches {

View file

@ -17,9 +17,9 @@ import { Switch } from "@components/Switch";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { IconComponent } from "@utils/types"; 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-"); const cl = classNameFactory("vc-plugin-ui-elements-");
@ -74,7 +74,7 @@ function Section(props: {
return ( return (
<section> <section>
<BaseText tag="h3" size="xl" weight="bold">{title}</BaseText> <BaseText tag="h3" size="lg" weight="semibold">{title}</BaseText>
<Paragraph size="sm" className={classes(Margins.top8, Margins.bottom20)}>{description}</Paragraph> <Paragraph size="sm" className={classes(Margins.top8, Margins.bottom20)}>{description}</Paragraph>
<div className={cl("switches")}> <div className={cl("switches")}>
@ -89,12 +89,12 @@ function Section(props: {
); );
} }
function UIElementsModal(props: ModalProps) { function UIElementsModal(props: RenderModalProps) {
const { uiElements } = useSettings(["uiElements.*"]); const { uiElements } = useSettings(["uiElements.*"]);
return ( return (
<ModalRoot {...props} size={ModalSize.MEDIUM}> <Modal {...props} size="md" title="Manage plugin UI elements">
<ModalContent className={cl("modal-content")}> <div className={cl("modal-content")}>
<Section <Section
title="Chatbar Buttons" title="Chatbar Buttons"
description="These are the buttons on the right side of the chat input bar" description="These are the buttons on the right side of the chat input bar"
@ -107,7 +107,7 @@ function UIElementsModal(props: ModalProps) {
buttonMap={MessagePopoverButtonMap} buttonMap={MessagePopoverButtonMap}
settings={uiElements.messagePopoverButtons} settings={uiElements.messagePopoverButtons}
/> />
</ModalContent> </div>
</ModalRoot> </Modal>
); );
} }

View file

@ -22,6 +22,7 @@ interface SettingBaseProps<T> {
}; };
id: string; id: string;
definedSettings: DefinedSettings; definedSettings: DefinedSettings;
closePluginSettings(): void;
} }
export type SettingProps<T extends PluginSettingDefCommon> = SettingBaseProps<T>; export type SettingProps<T extends PluginSettingDefCommon> = SettingBaseProps<T>;

View file

@ -20,6 +20,6 @@ import { PluginSettingComponentDef } from "@utils/types";
import { ComponentSettingProps } from "./Common"; import { ComponentSettingProps } from "./Common";
export function ComponentSetting({ setting, onChange }: ComponentSettingProps<PluginSettingComponentDef>) { export function ComponentSetting({ setting, onChange, closePluginSettings }: ComponentSettingProps<PluginSettingComponentDef>) {
return setting.component({ setValue: onChange, option: setting }); return setting.component({ setValue: onChange, option: setting, closePluginSettings });
} }

View file

@ -35,10 +35,10 @@ import { isTruthy } from "@utils/guards";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { useAwaiter, useIntersection } from "@utils/react"; import { useAwaiter, useCleanupEffect, useIntersection } from "@utils/react";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { PluginTag, PluginTags } from "@utils/types"; 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 { JSX } from "react";
import Plugins, { ExcludedPlugins, PluginMeta } from "~plugins"; import Plugins, { ExcludedPlugins, PluginMeta } from "~plugins";
@ -148,7 +148,7 @@ export default function PluginSettings() {
const changeRef = useRef<ChangeList<string>>(null); const changeRef = useRef<ChangeList<string>>(null);
const changes = changeRef.current ??= new ChangeList<string>(); const changes = changeRef.current ??= new ChangeList<string>();
React.useEffect(() => { useCleanupEffect(() => {
return () => { return () => {
if (!changes.hasChanges) return; if (!changes.hasChanges) return;
@ -158,23 +158,29 @@ export default function PluginSettings() {
const displayed = pluginNames.slice(0, maxDisplay); const displayed = pluginNames.slice(0, maxDisplay);
const remainingCount = pluginNames.length - displayed.length; const remainingCount = pluginNames.length - displayed.length;
Alerts.show({ openModal(props => (
title: t("vencord.settings.restartRequired.title"), <ConfirmModal
body: ( {...props}
<div> title={t("vencord.settings.restartRequired.title")}
{displayed.map((s, i) => ( confirmText={t("vencord.settings.restartRequired.confirm")}
<span key={i}> cancelText={t("vencord.settings.restartRequired.cancel")}
{i > 0 && ", "} variant="primary"
{Parser.parse("`" + s + "`")} onConfirm={() => location.reload()}
</span> >
))} <>
{remainingCount > 0 && <span> and {remainingCount} more</span>} <p>{t("vencord.settings.restartRequired.plugins")}</p>
</div> <div>
), {displayed.map((s, i) => (
confirmText: t("vencord.settings.restartRequired.confirm"), <span key={i}>
cancelText: t("vencord.settings.restartRequired.cancel"), {i > 0 && ", "}
onConfirm: () => location.reload() {Parser.parse("`" + s + "`")}
}); </span>
))}
{remainingCount > 0 && <span> and {remainingCount} more</span>}
</div>
</>
</ConfirmModal>
));
}; };
}, []); }, []);

View file

@ -88,12 +88,4 @@
.vc-cloud-button-icon { .vc-cloud-button-icon {
height: 1.25em; height: 1.25em;
}
.vc-settings-modal-root {
background: var(--background-base-low);
}
.vc-settings-modal {
padding-bottom: 1em;
} }

View file

@ -14,7 +14,7 @@ import { Span } from "@components/Span";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { relaunch } from "@utils/native"; import { relaunch } from "@utils/native";
import { changes, checkForUpdates, update, updateError } from "@utils/updater"; 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"; import { runWithDispatch } from "./runWithDispatch";
@ -113,17 +113,21 @@ export function Updatable(props: CommonProps) {
setUpdates([]); setUpdates([]);
await new Promise<void>(r => { await new Promise<void>(r => {
Alerts.show({ openModal(props => (
title: "Update Success!", <ConfirmModal
body: "Successfully updated. Restart now to apply the changes?", {...props}
confirmText: "Restart", title="Update Success!"
cancelText: "Not now!", subtitle="Successfully updated. Restart now to apply the changes?"
onConfirm() { confirmText="Restart"
relaunch(); cancelText="Not now!"
r(); variant="primary"
}, onConfirm={() => {
onCancel: r relaunch();
}); r();
}}
onCancel={r}
/>
));
}); });
} }
})} })}

View file

@ -6,7 +6,7 @@
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { UpdateLogger } from "@utils/updater"; import { UpdateLogger } from "@utils/updater";
import { Alerts, Parser } from "@webpack/common"; import { ConfirmModal,openModal, Parser } from "@webpack/common";
function getErrorMessage(e: any) { function getErrorMessage(e: any) {
if (!e?.code || !e.cmd) if (!e?.code || !e.cmd)
@ -33,16 +33,20 @@ export function runWithDispatch(dispatch: React.Dispatch<React.SetStateAction<bo
const err = getErrorMessage(e); const err = getErrorMessage(e);
Alerts.show({ openModal(props => (
title: "Oops!", <ConfirmModal
body: ( {...props}
title="Oops!"
confirmText="OK"
variant="primary"
>
<ErrorCard> <ErrorCard>
{err.split("\n").map((line, idx) => {err.split("\n").map((line, idx) =>
<div key={idx}>{Parser.parse(line)}</div> <div key={idx}>{Parser.parse(line)}</div>
)} )}
</ErrorCard> </ErrorCard>
) </ConfirmModal>
}); ));
} finally { } finally {
dispatch(false); dispatch(false);
} }

View file

@ -7,80 +7,81 @@
import { useSettings } from "@api/Settings"; import { useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Heading } from "@components/Heading"; import { Heading } from "@components/Heading";
import { IS_MAC } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { identity } from "@utils/misc"; import { identity } from "@utils/misc";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { Select } from "@webpack/common"; import { Select } from "@webpack/common";
export function VibrancySettings() { export function MacOSVibrancySettings() {
const settings = useSettings(["macosVibrancyStyle"]); const settings = useSettings(["macosVibrancyStyle"]);
if (!IS_MAC || IS_WEB) return null;
return ( return (
<> <ErrorBoundary noop>
<Heading>{t("vencord.settings.macosVibrancyStyle.title")}</Heading> <Heading tag="h5">{t("vencord.settings.macosVibrancyStyle.title")}</Heading>
<ErrorBoundary noop> <Select
<Select className={Margins.bottom20}
className={Margins.bottom20} placeholder={t("vencord.settings.macosVibrancyStyle.placeholder")}
placeholder={t("vencord.settings.macosVibrancyStyle.placeholder")} options={[
options={[ // Sorted from most opaque to most transparent
// Sorted from most opaque to most transparent {
{ label: "No vibrancy", value: undefined
label: "No vibrancy", value: undefined },
}, {
{ label: "Under Page (window tinting)",
label: "Under Page (window tinting)", value: "under-page"
value: "under-page" },
}, {
{ label: "Content",
label: "Content", value: "content"
value: "content" },
}, {
{ label: "Window",
label: "Window", value: "window"
value: "window" },
}, {
{ label: "Selection",
label: "Selection", value: "selection"
value: "selection" },
}, {
{ label: "Titlebar",
label: "Titlebar", value: "titlebar"
value: "titlebar" },
}, {
{ label: "Header",
label: "Header", value: "header"
value: "header" },
}, {
{ label: "Sidebar",
label: "Sidebar", value: "sidebar"
value: "sidebar" },
}, {
{ label: "Tooltip",
label: "Tooltip", value: "tooltip"
value: "tooltip" },
}, {
{ label: "Menu",
label: "Menu", value: "menu"
value: "menu" },
}, {
{ label: "Popover",
label: "Popover", value: "popover"
value: "popover" },
}, {
{ label: "Fullscreen UI (transparent but slightly muted)",
label: "Fullscreen UI (transparent but slightly muted)", value: "fullscreen-ui"
value: "fullscreen-ui" },
}, {
{ label: "HUD (Most transparent)",
label: "HUD (Most transparent)", value: "hud"
value: "hud" },
}, ]}
]} select={v => settings.macosVibrancyStyle = v}
select={v => settings.macosVibrancyStyle = v} isSelected={v => settings.macosVibrancyStyle === v}
isSelected={v => settings.macosVibrancyStyle === v} serialize={identity}
serialize={identity} />
/> </ErrorBoundary>
</ErrorBoundary>
</>
); );
} }

View file

@ -6,7 +6,6 @@
import { openNotificationLogModal } from "@api/Notifications/notificationLog"; import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { useSettings } from "@api/Settings"; import { useSettings } from "@api/Settings";
import { BaseText } from "@components/BaseText";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
@ -15,9 +14,8 @@ import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph"; import { Paragraph } from "@components/Paragraph";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { identity } from "@utils/misc"; import { identity } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { Select, Slider } from "@webpack/common"; import { Modal, openModal, Select, Slider } from "@webpack/common";
export function NotificationSection() { export function NotificationSection() {
return ( return (
@ -41,16 +39,13 @@ export function NotificationSection() {
export function openNotificationSettingsModal() { export function openNotificationSettingsModal() {
openModal(props => ( openModal(props => (
<ModalRoot {...props} size={ModalSize.MEDIUM}> <Modal
<ModalHeader> {...props}
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>Notification Settings</BaseText> size="lg"
<ModalCloseButton onClick={props.onClose} /> title={t("api.notifications.notificationSettings")}
</ModalHeader> >
<NotificationSettings />
<ModalContent> </Modal>
<NotificationSettings />
</ModalContent>
</ModalRoot>
)); ));
} }
@ -58,8 +53,8 @@ function NotificationSettings() {
const settings = useSettings(["notifications.*"]).notifications; const settings = useSettings(["notifications.*"]).notifications;
return ( return (
<div style={{ padding: "1em 0" }}> <>
<Heading>Notification Style</Heading> <Heading tag="h5">Notification Style</Heading>
{settings.useNative !== "never" && Notification?.permission === "denied" && ( {settings.useNative !== "never" && Notification?.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}> <ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
<Heading>Desktop Notification Permission denied</Heading> <Heading>Desktop Notification Permission denied</Heading>
@ -135,6 +130,6 @@ function NotificationSettings() {
onValueRender={v => v === 200 ? "∞" : v} onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v} onMarkerRender={v => v === 200 ? "∞" : v}
/> />
</div> </>
); );
} }

View file

@ -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 (
<ErrorBoundary noop>
<Heading tag="h5">Background Material</Heading>
<Paragraph className={Margins.bottom8}>
Windows transparent background effects. You need a theme that supports transparency or this will do nothing. A restart is required after changing this setting.
</Paragraph>
<Select
placeholder="None"
options={[
{
label: "None",
value: "none",
default: true
},
{
label: "Mica (incorporates system theme + desktop wallpaper to paint the background)",
value: "mica"
},
{
label: "Tabbed (variant of Mica with stronger background tinting)",
value: "tabbed"
},
{
label: "Acrylic (blurs the window behind Vesktop for a translucent background)",
value: "acrylic"
}
]}
closeOnSelect={true}
select={v => (settings.windowsMaterial = v)}
isSelected={v => v === settings.windowsMaterial}
serialize={s => s}
/>
</ErrorBoundary>
);
}

View file

@ -11,7 +11,6 @@ import { plugins } from "@api/PluginManager";
import { useSettings } from "@api/Settings"; import { useSettings } from "@api/Settings";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { Divider } from "@components/Divider"; import { Divider } from "@components/Divider";
import { Flex } from "@components/Flex";
import { FormSwitch } from "@components/FormSwitch"; import { FormSwitch } from "@components/FormSwitch";
import { Heading } from "@components/Heading"; import { Heading } from "@components/Heading";
import { FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "@components/Icons"; import { FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "@components/Icons";
@ -22,16 +21,18 @@ import { QuickAction, QuickActionCard } from "@components/settings/QuickAction";
import { SpecialCard } from "@components/settings/SpecialCard"; import { SpecialCard } from "@components/settings/SpecialCard";
import BadgeAPI from "@plugins/_api/badges"; import BadgeAPI from "@plugins/_api/badges";
import { gitRemote } from "@shared/vencordUserAgent"; import { gitRemote } from "@shared/vencordUserAgent";
import { DONOR_ROLE_ID, GUILD_ID, IS_MAC, IS_WINDOWS, VC_DONOR_ROLE_ID, VC_GUILD_ID } from "@utils/constants"; import { DONOR_ROLE_ID, GUILD_ID, IS_WINDOWS, VC_DONOR_ROLE_ID, VC_GUILD_ID } from "@utils/constants";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { identity, isAnyPluginDev } from "@utils/misc"; import { isAnyPluginDev } from "@utils/misc";
import { relaunch } from "@utils/native"; import { relaunch } from "@utils/native";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { Alerts, GuildMemberStore, React, Select, UserStore } from "@webpack/common"; import { Alerts, GuildMemberStore, React, useMemo, UserStore } from "@webpack/common";
import { DonateButtonComponent } from "./DonateButton"; import { DonateButtonComponent } from "./DonateButton";
import { openNotificationSettingsModal } from "./NotificationSettings"; import { MacOSVibrancySettings } from "./MacVibrancySettings";
import { NotificationSection } from "./NotificationSettings";
import { WindowsMaterialSettings } from "./WindowsMaterialSettings";
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png"; const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
const SHIGGY_DONATE_IMAGE = "https://equicord.org/assets/favicon.png"; const SHIGGY_DONATE_IMAGE = "https://equicord.org/assets/favicon.png";
@ -48,80 +49,121 @@ type KeysOfType<Object, Type> = {
[K in keyof Object]: Object[K] extends Type ? K : never; [K in keyof Object]: Object[K] extends Type ? K : never;
}[keyof Object]; }[keyof Object];
function EquicordSettings() { function Switches() {
const settings = useSettings(); const settings = useSettings(["useQuickCss", "enableReactDevtools", "mainWindowFrameless", "frameless", "winNativeTitleBar", "transparent", "winCtrlQ", "disableMinSize"]);
const donateImage = React.useMemo( const Switches = [
() => (Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE), {
[], key: "useQuickCss",
); title: t("vencord.settings.useQuickCss.title"),
description: t("vencord.settings.useQuickCss.description"),
const needsVibrancySettings = IS_DISCORD_DESKTOP && IS_MAC; },
!IS_WEB && {
const user = UserStore?.getCurrentUser(); key: "enableReactDevtools",
title: t("vencord.settings.enableReactDevtools.title"),
const Switches: Array<false | { description: t("vencord.settings.enableReactDevtools.description"),
restartRequired: true,
},
(!IS_WEB && !IS_DISCORD_DESKTOP || !IS_WINDOWS) && {
key: "mainWindowFrameless",
title: t("equicord.mainWindowFrameless.title"),
description: t("equicord.mainWindowFrameless.description"),
restartRequired: true,
},
!IS_WEB && (!IS_DISCORD_DESKTOP || !IS_WINDOWS
? {
key: "frameless",
title: t("vencord.settings.frameless.title"),
description: t("vencord.settings.frameless.description"),
restartRequired: true,
}
: {
key: "winNativeTitleBar",
title: t("vencord.settings.winNativeTitleBar.title"),
description: t("vencord.settings.winNativeTitleBar.description"),
restartRequired: true,
}
),
!IS_WEB && {
key: "transparent",
title: t("vencord.settings.transparent.title"),
description: t("vencord.settings.transparent.description"),
restartRequired: true,
warning: IS_WINDOWS
? t("vencord.settings.transparent.noteWindows")
: t("vencord.settings.transparent.note"),
},
IS_DISCORD_DESKTOP && {
key: "disableMinSize",
title: t("vencord.settings.disableMinSize.title"),
description: t("vencord.settings.disableMinSize.description"),
restartRequired: true,
},
!IS_WEB &&
IS_WINDOWS && {
key: "winCtrlQ",
title: t("vencord.settings.winCtrlQ.title"),
description: t("vencord.settings.winCtrlQ.description"),
restartRequired: true,
}
] satisfies Array<false | {
key: KeysOfType<typeof settings, boolean>; key: KeysOfType<typeof settings, boolean>;
title: string; title: string;
description?: string; description?: string;
restartRequired?: boolean; restartRequired?: boolean;
warning?: string; warning?: string;
}> }>;
= [
{ return Switches.map(setting => {
key: "useQuickCss", if (!setting) {
title: t("vencord.settings.useQuickCss.title"), return null;
description: t("vencord.settings.useQuickCss.description"), }
},
!IS_WEB && { const { key, title, description, restartRequired, warning } = setting;
key: "enableReactDevtools",
title: t("vencord.settings.enableReactDevtools.title"), return (
description: t("vencord.settings.enableReactDevtools.description"), <FormSwitch
restartRequired: true, key={key}
}, title={title}
(!IS_WEB && !IS_DISCORD_DESKTOP || !IS_WINDOWS) && { description={
key: "mainWindowFrameless", warning ? (
title: t("equicord.mainWindowFrameless.title"), <>
description: t("equicord.mainWindowFrameless.description"), {description}
restartRequired: true, <Notice.Warning className={Margins.top8} style={{ width: "100%" }}>
}, {warning}
!IS_WEB && (!IS_DISCORD_DESKTOP || !IS_WINDOWS </Notice.Warning>
? { </>
key: "frameless", ) : (
title: t("vencord.settings.frameless.title"), description
description: t("vencord.settings.frameless.description"), )
restartRequired: true,
} }
: { value={settings[key]}
key: "winNativeTitleBar", onChange={v => {
title: t("vencord.settings.winNativeTitleBar.title"), settings[key] = v;
description: t("vencord.settings.winNativeTitleBar.description"),
restartRequired: true, if (restartRequired) {
} Alerts.show({
), title: t("vencord.settings.restartRequired.title"),
!IS_WEB && { body: t("vencord.settings.restartRequired.body"),
key: "transparent", confirmText: t("vencord.settings.restartRequired.confirm"),
title: t("vencord.settings.transparent.title"), cancelText: t("vencord.settings.restartRequired.cancel"),
description: t("vencord.settings.transparent.description"), onConfirm: relaunch
restartRequired: true, });
warning: IS_WINDOWS }
? t("vencord.settings.transparent.noteWindows") }}
: t("vencord.settings.transparent.note"), hideBorder
}, />
IS_DISCORD_DESKTOP && { );
key: "disableMinSize", });
title: t("vencord.settings.disableMinSize.title"), }
description: t("vencord.settings.disableMinSize.description"),
restartRequired: true, function EquicordSettings() {
}, const donateImage = useMemo(() =>
!IS_WEB && Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE,
IS_WINDOWS && { []
key: "winCtrlQ", );
title: t("vencord.settings.winCtrlQ.title"),
description: t("vencord.settings.winCtrlQ.description"), const user = UserStore?.getCurrentUser();
restartRequired: true,
}
];
return ( return (
<SettingsTab> <SettingsTab>
@ -233,131 +275,13 @@ function EquicordSettings() {
</a>. </a>.
</Notice.Info> </Notice.Info>
{Switches.filter((s): s is Exclude<typeof s, false> => !!s).map( <Switches />
s => (
<FormSwitch
key={s.key}
value={settings[s.key]}
onChange={v => {
settings[s.key] = v;
if (s.restartRequired) { <MacOSVibrancySettings />
Alerts.show({ <WindowsMaterialSettings />
title: "Restart Required",
body: "A restart is required to apply this change",
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: relaunch
});
}
}}
title={s.title}
description={
s.warning ? (
<>
{s.description}
<Notice.Warning className={Margins.top8} style={{ width: "100%" }}>
{s.warning}
</Notice.Warning>
</>
) : (
s.description
)
}
hideBorder
/>
),
)}
{needsVibrancySettings && ( <NotificationSection />
<> </SettingsTab >
<Divider className={Margins.top20} />
<Heading className={Margins.top20}>Window Vibrancy</Heading>
<Paragraph className={Margins.bottom16}>
Customize the macOS window vibrancy effect. This controls the blur and transparency style of the Discord window. Changes require a restart to take effect.
</Paragraph>
<Select
className={Margins.bottom20}
placeholder="Window vibrancy style"
options={[
// Sorted from most opaque to most transparent
{
label: "No vibrancy",
value: undefined,
},
{
label: "Under Page (window tinting)",
value: "under-page",
},
{
label: "Content",
value: "content",
},
{
label: "Window",
value: "window",
},
{
label: "Selection",
value: "selection",
},
{
label: "Titlebar",
value: "titlebar",
},
{
label: "Header",
value: "header",
},
{
label: "Sidebar",
value: "sidebar",
},
{
label: "Tooltip",
value: "tooltip",
},
{
label: "Menu",
value: "menu",
},
{
label: "Popover",
value: "popover",
},
{
label: "Fullscreen UI (transparent but slightly muted)",
value: "fullscreen-ui",
},
{
label: "HUD (Most transparent)",
value: "hud",
},
]}
select={v => (settings.macosVibrancyStyle = v)}
isSelected={v => settings.macosVibrancyStyle === v}
serialize={identity}
/>
</>
)}
<Divider className={Margins.top20} />
<Heading className={Margins.top20}>Notifications</Heading>
<Paragraph className={Margins.bottom16}>
Configure how Equicord handles notifications. You can customize when and how you receive alerts, or view a history of past notifications.
</Paragraph>
<Flex gap="16px">
<Button onClick={openNotificationSettingsModal}>
Notification Settings
</Button>
<Button variant="secondary" onClick={openNotificationLogModal}>
View Notification Log
</Button>
</Flex>
</SettingsTab>
); );
} }

View file

@ -12,6 +12,7 @@ export default definePlugin({
description: "API to add collections to the user profile panel like discords game collection.", description: "API to add collections to the user profile panel like discords game collection.",
authors: [Devs.thororen], authors: [Devs.thororen],
patches: [ patches: [
// message and member list popouts
{ {
find: "#{intl::USER_PROFILE_FRIEND_REQUEST_TOAST}", find: "#{intl::USER_PROFILE_FRIEND_REQUEST_TOAST}",
replacement: { replacement: {
@ -19,6 +20,7 @@ export default definePlugin({
replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections(arguments[0]),", replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections(arguments[0]),",
} }
}, },
// user panel popout
{ {
find: '"UserProfileAccountPopout"', find: '"UserProfileAccountPopout"',
replacement: { replacement: {
@ -26,6 +28,7 @@ export default definePlugin({
replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections(arguments[0]),", replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections(arguments[0]),",
}, },
}, },
// dm sidebar
{ {
find: ".SIDEBAR,disableToolbar:", find: ".SIDEBAR,disableToolbar:",
replacement: { replacement: {

View file

@ -12,15 +12,15 @@ export default definePlugin({
description: "API to add sections near the 'Member Since' area of user profile panels.", description: "API to add sections near the 'Member Since' area of user profile panels.",
authors: [Devs.thororen], authors: [Devs.thororen],
patches: [ patches: [
// DM User Sidebar // dm user sidebar
{ {
find: "#{intl::PREMIUM_GIFTING_BUTTON}),action:", find: '"UserProfileSidebar"',
replacement: { replacement: {
match: /(#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id)}\)}\))/, match: /(#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id)}\)}\))/,
replace: "$1,Vencord.Api.ProfileSections.renderProfileSections({userId:$2,isSideBar:true})", replace: "$1,Vencord.Api.ProfileSections.renderProfileSections({userId:$2,isSideBar:true})",
} }
}, },
// User Profile Modal // user profile modal
{ {
find: ",applicationRoleConnection:", find: ",applicationRoleConnection:",
replacement: { replacement: {
@ -28,7 +28,7 @@ export default definePlugin({
replace: "$1,Vencord.Api.ProfileSections.renderProfileSections({userId:$2,isSideBar:false}),", replace: "$1,Vencord.Api.ProfileSections.renderProfileSections({userId:$2,isSideBar:false}),",
} }
}, },
// User Profile Modal v2 // user profile modal v2
{ {
find: ".MODAL_V2,onClose:", find: ".MODAL_V2,onClose:",
replacement: { replacement: {

View file

@ -11,7 +11,7 @@ import { t } from "@utils/translation";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findCssClassesLazy } from "@webpack"; import { findCssClassesLazy } from "@webpack";
const { iconForeground } = findCssClassesLazy("iconForeground", "accountPopoutButtonWrapper"); const accountClasses = findCssClassesLazy("iconForeground", "accountPopoutButtonWrapper");
export default definePlugin({ export default definePlugin({
name: "UserAreaAPI", name: "UserAreaAPI",
@ -38,7 +38,7 @@ export default definePlugin({
renderButtons(props: { nameplate?: any; }) { renderButtons(props: { nameplate?: any; }) {
return Vencord.Api.UserArea._renderButtons({ return Vencord.Api.UserArea._renderButtons({
nameplate: props.nameplate, nameplate: props.nameplate,
iconForeground: props.nameplate != null ? iconForeground : void 0, iconForeground: accountClasses.iconForeground,
hideTooltips: this.shouldHideTooltips() hideTooltips: this.shouldHideTooltips()
}); });
}, },

View file

@ -14,19 +14,20 @@ export default definePlugin({
name: "ConcatenatedModules", name: "ConcatenatedModules",
description: "Extract modules that have been concatenated by the bundler", description: "Extract modules that have been concatenated by the bundler",
authors: [Devs.thororen], authors: [Devs.thororen],
required: true,
patches: [ patches: [
{ {
find: "AngleBracketsIcon", find: "AngleBracketsIcon",
replacement: { replacement: {
match: /\i\.\i\((\i)\),\i\.\i\(\i,\{AIcon/, match: /\i\.\i\((\i)\),\i\.\i\(\i,\{AIcon/,
replace: "$self.iconsModule($1),$&" replace: "$self.setIconsModule($1),$&"
} }
} }
], ],
iconsModule(value: IconsDef) { setIconsModule(value: IconsDef) {
iconsModule = value; 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;
}, },
}); });

View file

@ -17,17 +17,14 @@
*/ */
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { BaseText } from "@components/BaseText";
import { CodeBlock } from "@components/CodeBlock"; import { CodeBlock } from "@components/CodeBlock";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Heading } from "@components/Heading"; import { Heading } from "@components/Heading";
import { EquicordDevs } from "@utils/constants"; import { EquicordDevs } from "@utils/constants";
import { copyWithToast } from "@utils/discord"; import { copyWithToast } from "@utils/discord";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore } from "@webpack/common"; import { ChannelStore, Modal, openModal } from "@webpack/common";
function DecodeIcon() { function DecodeIcon() {
return ( return (
@ -65,32 +62,26 @@ function decodeBase64Strings(base64Strings) {
} }
function openDecodedBase64Modal(decodedContent) { function openDecodedBase64Modal(decodedContent) {
const key = openModal(props => ( openModal(props => (
<ErrorBoundary> <ErrorBoundary>
<ModalRoot {...props} size={ModalSize.LARGE}> <Modal
<ModalHeader> {...props}
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>{t("equicord.baseDecoder.ui.modalTitle")}</BaseText> size="lg"
<ModalCloseButton onClick={() => closeModal(key)} /> title={t("equicord.baseDecoder.ui.modalTitle")}
</ModalHeader> actions={decodedContent.map((content, index) => ({
<ModalContent> text: `${t("equicord.baseDecoder.ui.copyDecoded")} ${index + 1}`,
<div style={{ padding: "16px 0" }}> variant: "primary",
<Heading>{t("equicord.baseDecoder.ui.decodedContent")}</Heading> onClick: () => copyWithToast(content, t("equicord.baseDecoder.ui.copiedToast"))
{decodedContent.map((content, index) => ( }))}
<CodeBlock key={index} content={content} lang="" /> >
))} <div style={{ padding: "16px 0" }}>
</div> <Heading>{t("equicord.baseDecoder.ui.decodedContent")}</Heading>
</ModalContent > {decodedContent.map((content, index) => (
<ModalFooter> <CodeBlock key={index} content={content} lang="" />
<Flex gap={10}> ))}
{decodedContent.map((content, index) => ( </div>
<Button key={index} onClick={() => copyWithToast(content, t("equicord.baseDecoder.ui.copiedToast"))}> </Modal>
{t("equicord.baseDecoder.ui.copyDecoded")} {index + 1} </ErrorBoundary>
</Button>
))}
</Flex>
</ModalFooter>
</ModalRoot >
</ErrorBoundary >
)); ));
} }

View file

@ -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<RegExp>;
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 (
<Card className={cl("regex")}>
<HeadingTertiary className={Margins.bottom8}>Regex Helper</HeadingTertiary>
<TextInput
type="text"
placeholder="Input to test..."
value={testInput}
onChange={setTestInput}
maxLength={null}
/>
{results.length === 0 ?
<Card
key="vc-no-patterns-regex"
variant="warning"
className={classes(cl("card"), Margins.top8)}
>
<code>No patterns configured</code>
</Card> : (
results.map(({ pattern, matches, error }, i) => (
<Card
key={`vc-pattern-card-${i}`}
variant={error ? "danger" : matches ? "success" : "primary"}
className={classes(cl("card"), Margins.top8)}
>
<code>{pattern}</code>
{error && <span className={cl("error")}>{error}</span>}
</Card>
)))}
</Card>
);
}
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: () => <ErrorBoundary noop><RegexHelper /></ErrorBoundary>,
},
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))
));
}
});

View file

@ -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;
}

View file

@ -10,9 +10,8 @@ import { CircleQuestionIcon, DiscoveryIcon, EnvelopeIcon, FriendsIcon, NitroIcon
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { getGuildAcronym, getIntlMessage } from "@utils/discord"; import { getGuildAcronym, getIntlMessage } from "@utils/discord";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { closeModal, openModal } from "@utils/modal";
import { findComponentByCodeLazy } from "@webpack"; 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 { NotificationDot } from "./ChannelTab";
import { BookmarkContextMenu, EditModal } from "./ContextMenus"; import { BookmarkContextMenu, EditModal } from "./ContextMenus";

View file

@ -11,9 +11,9 @@ import { bookmarkFolderColors, bookmarkPlaceholderName, closeOtherTabs, closeTab
import { Bookmark, BookmarkFolder, Bookmarks, ChannelTabsProps, UseBookmarkMethods } from "@equicordplugins/channelTabs/util/types"; import { Bookmark, BookmarkFolder, Bookmarks, ChannelTabsProps, UseBookmarkMethods } from "@equicordplugins/channelTabs/util/types";
import { getIntlMessage } from "@utils/discord"; import { getIntlMessage } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import { t } from "@utils/translation"; 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<string, string> = { const legacyFolderColors: Record<string, string> = {
"var(--channeltabs-red)": bookmarkFolderColors.Red, "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 }: { function FolderIconPickerModal({ modalProps, modalKey, name, color, iconName, onSelect, onColorChange }: {
modalProps: ModalProps, modalProps: RenderModalProps,
modalKey: string, modalKey: string,
name: string, name: string,
color: string, color: string,
@ -134,73 +134,72 @@ function FolderIconPickerModal({ modalProps, modalKey, name, color, iconName, on
}, [iconNames, search]); }, [iconNames, search]);
return ( return (
<ModalRoot {...modalProps}> <Modal
<ModalHeader> {...modalProps}
<BaseText size="lg" weight="semibold">Choose Folder Icon</BaseText> size="sm"
</ModalHeader> title={<BaseText size="lg" weight="semibold">Choose Folder Icon</BaseText>}
<ModalContent> actions={[
<Heading className={Margins.top16}>Preview</Heading> {
<FolderChipPreview name={name} color={localColor} iconName={iconName} /> text: "Close",
<Heading className={Margins.top16}>Icon Color</Heading> variant: "secondary",
<div className={Margins.top8}> onClick: () => closeModal(modalKey)
<ColorPicker }
color={colorToInt(localColor)} ]}
onChange={value => { >
const nextColor = intToColor(value); <Heading className={Margins.top16}>Preview</Heading>
setLocalColor(nextColor); <FolderChipPreview name={name} color={localColor} iconName={iconName} />
onColorChange(nextColor); <Heading className={Margins.top16}>Icon Color</Heading>
}} <div className={Margins.top8}>
showEyeDropper={false} <ColorPicker
/> color={colorToInt(localColor)}
</div> onChange={value => {
<Heading className={Margins.top16}>Search</Heading> const nextColor = intToColor(value);
<TextInput setLocalColor(nextColor);
value={search} onColorChange(nextColor);
placeholder={`Search ${iconNames.length} icons...`} }}
onChange={setSearch} showEyeDropper={false}
/> />
<div style={{ </div>
display: "grid", <Heading className={Margins.top16}>Search</Heading>
gap: 8, <TextInput
gridTemplateColumns: "repeat(auto-fill, minmax(52px, 1fr))", value={search}
marginTop: 16, placeholder={`Search ${iconNames.length} icons...`}
maxHeight: 320, onChange={setSearch}
overflowY: "auto" />
}}> <div style={{
{filteredIconNames.map(name => ( display: "grid",
<button gap: 8,
key={name} gridTemplateColumns: "repeat(auto-fill, minmax(52px, 1fr))",
onClick={() => { marginTop: 16,
onSelect(name); maxHeight: 320,
closeModal(modalKey); overflowY: "auto"
}} }}>
style={{ {filteredIconNames.map(name => (
alignItems: "center", <button
background: name === iconName ? "var(--background-modifier-hover)" : "var(--background-secondary)", key={name}
border: "1px solid var(--border-subtle)", onClick={() => {
borderRadius: 8, onSelect(name);
cursor: "pointer", closeModal(modalKey);
display: "flex", }}
height: 52, style={{
justifyContent: "center", alignItems: "center",
padding: 0 background: name === iconName ? "var(--background-modifier-hover)" : "var(--background-secondary)",
}} border: "1px solid var(--border-subtle)",
title={name} borderRadius: 8,
type="button" cursor: "pointer",
> display: "flex",
<FolderGlyph color={localColor} iconName={name} /> height: 52,
</button> justifyContent: "center",
))} padding: 0
</div> }}
</ModalContent> title={name}
<ModalFooter> type="button"
<Button >
color={Button.Colors.TRANSPARENT} <FolderGlyph color={localColor} iconName={name} />
look={Button.Looks.FILLED} </button>
onClick={() => closeModal(modalKey)} ))}
>Close</Button> </div>
</ModalFooter> </Modal>
</ModalRoot>
); );
} }
@ -264,7 +263,7 @@ function FolderAppearanceFields({
} }
export function EditModal({ modalProps, modalKey, bookmark, onSave }: { export function EditModal({ modalProps, modalKey, bookmark, onSave }: {
modalProps: ModalProps, modalProps: RenderModalProps,
modalKey: string, modalKey: string,
bookmark: Bookmark | BookmarkFolder, bookmark: Bookmark | BookmarkFolder,
onSave: (name: string, color: string, iconName?: string) => void; onSave: (name: string, color: string, iconName?: string) => void;
@ -275,48 +274,47 @@ export function EditModal({ modalProps, modalKey, bookmark, onSave }: {
const placeholder = bookmarkPlaceholderName(bookmark); const placeholder = bookmarkPlaceholderName(bookmark);
return ( return (
<ModalRoot {...modalProps}> <Modal
<ModalHeader> {...modalProps}
<BaseText size="lg" weight="semibold">Edit Bookmark</BaseText> size="sm"
</ModalHeader> title={<BaseText size="lg" weight="semibold">Edit Bookmark</BaseText>}
<ModalContent> actions={[
{isBookmarkFolder(bookmark) {
? <FolderAppearanceFields text: "Save",
name={name} variant: "primary",
setName={setName} onClick: () => onSave(name || placeholder, color, iconName)
color={color} },
setColor={setColor} {
iconName={iconName} text: t("vencord.cancel"),
setIconName={setIconName} variant: "secondary",
onClick: () => closeModal(modalKey)
}
]}
>
{isBookmarkFolder(bookmark)
? <FolderAppearanceFields
name={name}
setName={setName}
color={color}
setColor={setColor}
iconName={iconName}
setIconName={setIconName}
placeholder={placeholder}
/>
: <>
<Heading className={Margins.top16}>Bookmark Name</Heading>
<TextInput
value={name === placeholder ? undefined : name}
placeholder={placeholder} placeholder={placeholder}
onChange={setName}
/> />
: <> </>}
<Heading className={Margins.top16}>Bookmark Name</Heading> </Modal>
<TextInput
value={name === placeholder ? undefined : name}
placeholder={placeholder}
onChange={setName}
/>
</>}
</ModalContent>
<ModalFooter>
<Button
onClick={() => onSave(name || placeholder, color, iconName)}
>Save</Button>
<Button
color={Button.Colors.TRANSPARENT}
look={Button.Looks.FILLED}
onClick={() => closeModal(modalKey)}
>
{t("vencord.cancel")}
</Button>
</ModalFooter>
</ModalRoot>
); );
} }
function AddToFolderModal({ modalProps, modalKey, bookmarks, onSave }: { function AddToFolderModal({ modalProps, modalKey, bookmarks, onSave }: {
modalProps: any, modalProps: RenderModalProps,
modalKey: string, modalKey: string,
bookmarks: Bookmarks, bookmarks: Bookmarks,
onSave: (folderIndex: number, folderName: string, folderColor: string, iconName?: string) => void; onSave: (folderIndex: number, folderName: string, folderColor: string, iconName?: string) => void;
@ -327,80 +325,79 @@ function AddToFolderModal({ modalProps, modalKey, bookmarks, onSave }: {
const [iconName, setIconName] = useState<string | undefined>(); const [iconName, setIconName] = useState<string | undefined>();
return ( return (
<ModalRoot {...modalProps}> <Modal
<ModalHeader> {...modalProps}
<BaseText size="lg" weight="semibold">Add Bookmark to Folder</BaseText> size="sm"
</ModalHeader> title={<BaseText size="lg" weight="semibold">Add Bookmark to Folder</BaseText>}
<ModalContent> actions={[
<Heading className={Margins.top16}>Select a folder</Heading> {
<Select text: "Save",
options={[...Object.entries(bookmarks) variant: "primary",
.filter(([, bookmark]) => isBookmarkFolder(bookmark)) onClick: () => onSave(folderIndex, folderName, folderColor, iconName)
.map(([index, bookmark]) => ({ },
label: bookmark.name, {
value: parseInt(index, 10) text: t("vencord.cancel"),
})), variant: "secondary",
{ onClick: () => closeModal(modalKey)
label: "Create one", }
value: -1, ]}
default: true >
}]} <Heading className={Margins.top16}>Select a folder</Heading>
isSelected={v => v === folderIndex} <Select
select={setIndex} options={[...Object.entries(bookmarks)
serialize={String} .filter(([, bookmark]) => isBookmarkFolder(bookmark))
/> .map(([index, bookmark]) => ({
{folderIndex === -1 && <FolderAppearanceFields label: bookmark.name,
name={folderName} value: parseInt(index, 10)
setName={setFolderName} })),
color={folderColor} {
setColor={setFolderColor} label: "Create one",
iconName={iconName} value: -1,
setIconName={setIconName} default: true
/>} }]}
</ModalContent> isSelected={v => v === folderIndex}
<ModalFooter> select={setIndex}
<Button serialize={String}
onClick={() => onSave(folderIndex, folderName, folderColor, iconName)} />
>Save</Button> {folderIndex === -1 && <FolderAppearanceFields
<Button name={folderName}
color={Button.Colors.TRANSPARENT} setName={setFolderName}
look={Button.Looks.FILLED} color={folderColor}
onClick={() => closeModal(modalKey)} setColor={setFolderColor}
> iconName={iconName}
{t("vencord.cancel")} setIconName={setIconName}
</Button> />}
</ModalFooter> </Modal>
</ModalRoot>
); );
} }
function DeleteFolderConfirmationModal({ modalProps, modalKey, onConfirm }) { function DeleteFolderConfirmationModal({ modalProps, modalKey, onConfirm }: {
modalProps: RenderModalProps,
modalKey: string,
onConfirm: () => void;
}) {
return ( return (
<ModalRoot {...modalProps}> <Modal
<ModalHeader> {...modalProps}
<BaseText size="lg" weight="semibold">Are you sure?</BaseText> size="sm"
</ModalHeader> title={<BaseText size="lg" weight="semibold">Are you sure?</BaseText>}
<ModalContent> actions={[
<Paragraph className={Margins.top16}> {
Deleting a bookmark folder will also delete all bookmarks within it. text: "Delete",
</Paragraph> variant: "critical-primary",
</ModalContent> onClick: onConfirm
<ModalFooter> },
<Button {
color={Button.Colors.RED} text: t("vencord.cancel"),
onClick={onConfirm} variant: "secondary",
> onClick: () => closeModal(modalKey)
Delete }
</Button> ]}
<Button >
color={Button.Colors.TRANSPARENT} <Paragraph className={Margins.top16}>
look={Button.Looks.FILLED} Deleting a bookmark folder will also delete all bookmarks within it.
onClick={() => closeModal(modalKey)} </Paragraph>
> </Modal>
{t("vencord.cancel")}
</Button>
</ModalFooter>
</ModalRoot>
); );
} }

View file

@ -50,7 +50,7 @@ export default definePlugin({
description: t("equicord.channelTabs.description"), description: t("equicord.channelTabs.description"),
tags: ["Appearance", "Customisation", "Organisation", "Servers"], tags: ["Appearance", "Customisation", "Organisation", "Servers"],
authors: [Devs.TheSun, Devs.TheKodeToad, EquicordDevs.keifufu, Devs.Nickyux, EquicordDevs.DiabeloDEV, EquicordDevs.justjxke, EquicordDevs.keircn], authors: [Devs.TheSun, Devs.TheKodeToad, EquicordDevs.keifufu, Devs.Nickyux, EquicordDevs.DiabeloDEV, EquicordDevs.justjxke, EquicordDevs.keircn],
dependencies: ["ContextMenuAPI"], dependencies: ["ContextMenuAPI", "ConcatenatedModules"],
contextMenus: { contextMenus: {
"channel-mention-context": contextMenuPatch, "channel-mention-context": contextMenuPatch,
"channel-context": contextMenuPatch, "channel-context": contextMenuPatch,

View file

@ -40,7 +40,7 @@ const settings = definePluginSettings({
}, },
enableSpeakingIndicators: { enableSpeakingIndicators: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Enable speaking indicators", description: t("equicord.clipsEnhancements.settings.enableSpeakingIndicators"),
default: true, default: true,
restartNeeded: true restartNeeded: true
}, },
@ -104,14 +104,6 @@ export default definePlugin({
replace: "$self.patchTimeslots($&)" 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; }[]) { patchTimeslots(timeslots: { id: string; value: number; label: string; }[]) {

View file

@ -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<typeof settings.use>;
function PanelsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="none" {...props}>
<path fill="currentColor" d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3H3V5Zm0 5h6v11H5a2 2 0 0 1-2-2V10Zm8 0h10v9a2 2 0 0 1-2 2H11V10Zm2-5h8v3h-8V5Z" />
</svg>
);
}
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 (
<Menu.Menu navId="vc-collapsible-ui-toolbar-menu" onClose={onClose} aria-label="Collapsible UI">
{toolbarPanelOrder.map(panelId => {
const panel = panelRegistry[panelId];
const collapsed = store[panel.collapsedKey];
return (
<Menu.MenuCheckboxItem
key={panelId}
id={`vc-collapsible-ui-${panel.classId}`}
label={panel.label}
checked={!collapsed}
action={() => togglePanel(panelId)}
/>
);
})}
</Menu.Menu>
);
}, { noop: true });
const ToolbarButtons = ErrorBoundary.wrap(() => {
const store = settings.use();
const anyCollapsed = toolbarPanelOrder.some(panelId => store[panelRegistry[panelId].collapsedKey]);
return (
<>
<span aria-hidden className={getStateClasses(store)} style={{ display: "none" }} />
<ChannelToolbarButton
icon={PanelsIcon}
tooltip="Collapsible UI"
aria-label="Collapsible UI"
selected={anyCollapsed}
onClick={event => ContextMenuApi.openContextMenu(event, () => <ToolbarMenu onClose={ContextMenuApi.closeContextMenu} />)}
onContextMenu={event => ContextMenuApi.openContextMenu(event, () => <ToolbarMenu onClose={ContextMenuApi.closeContextMenu} />)}
/>
</>
);
}, { noop: true });
const ChatButtonsRow = ErrorBoundary.wrap(({ buttons }: { buttons: ReactNode[]; }) => {
const { chatButtonsCollapsed } = settings.use();
if (buttons.length === 0) return <>{buttons}</>;
return (
<div
className={classes(
cl("chat-buttons"),
chatButtonsCollapsed && cl("chat-buttons-collapsed")
)}
>
<div className={cl("chat-buttons-items")}>
{buttons}
</div>
</div>
);
}, { 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: () => <ToolbarButtons />,
},
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 <ChatButtonsRow buttons={buttons} />;
},
userAreaControlsClass(className: string) {
return classes(className, cl("user-area-controls"));
},
});

View file

@ -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,
},
});

View file

@ -1,416 +0,0 @@
@property --vc-cui-collapsed-size {
syntax: "<length>";
inherits: true;
initial-value: 12px;
}
@property --vc-cui-transition {
syntax: "<time>";
inherits: true;
initial-value: 160ms;
}
:root {
--vc-cui-chat-buttons-width: min(320px, 36vw);
--vc-cui-hover-peek: var(--vc-cui-collapsed-size);
--vc-cui-hover-delay: 120ms;
--vc-cui-header-bar-height: 32px;
--vc-cui-user-area-collapsed-width: 52px;
--vc-cui-ease: cubic-bezier(.2, 0, 0, 1);
--vc-cui-fade-transition: opacity var(--vc-cui-transition) var(--vc-cui-ease);
--vc-cui-inline-transition:
width var(--vc-cui-transition) var(--vc-cui-ease),
inline-size var(--vc-cui-transition) var(--vc-cui-ease),
min-width var(--vc-cui-transition) var(--vc-cui-ease),
min-inline-size var(--vc-cui-transition) var(--vc-cui-ease),
max-width var(--vc-cui-transition) var(--vc-cui-ease),
max-inline-size var(--vc-cui-transition) var(--vc-cui-ease),
flex-basis var(--vc-cui-transition) var(--vc-cui-ease);
--vc-cui-block-transition:
height var(--vc-cui-transition) var(--vc-cui-ease),
block-size var(--vc-cui-transition) var(--vc-cui-ease),
min-height var(--vc-cui-transition) var(--vc-cui-ease),
min-block-size var(--vc-cui-transition) var(--vc-cui-ease),
max-height var(--vc-cui-transition) var(--vc-cui-ease),
max-block-size var(--vc-cui-transition) var(--vc-cui-ease),
padding var(--vc-cui-transition) var(--vc-cui-ease)
}
body:has(.vc-collapsible-ui-collapsed-size-8) {
--vc-cui-collapsed-size: 8px
}
body:has(.vc-collapsible-ui-collapsed-size-12) {
--vc-cui-collapsed-size: 12px
}
body:has(.vc-collapsible-ui-collapsed-size-16) {
--vc-cui-collapsed-size: 16px
}
body:has(.vc-collapsible-ui-collapsed-size-20) {
--vc-cui-collapsed-size: 20px
}
body:has(.vc-collapsible-ui-collapsed-size-24) {
--vc-cui-collapsed-size: 24px
}
body:has(.vc-collapsible-ui-collapsed-size-28) {
--vc-cui-collapsed-size: 28px
}
body:has(.vc-collapsible-ui-collapsed-size-32) {
--vc-cui-collapsed-size: 32px
}
body:has(.vc-collapsible-ui-collapsed-size-40) {
--vc-cui-collapsed-size: 40px
}
body:has(.vc-collapsible-ui-collapsed-size-48) {
--vc-cui-collapsed-size: 48px
}
body:has(.vc-collapsible-ui-transition-0) {
--vc-cui-transition: 0ms
}
body:has(.vc-collapsible-ui-transition-80) {
--vc-cui-transition: 80ms
}
body:has(.vc-collapsible-ui-transition-120) {
--vc-cui-transition: 120ms
}
body:has(.vc-collapsible-ui-transition-160) {
--vc-cui-transition: 160ms
}
body:has(.vc-collapsible-ui-transition-200) {
--vc-cui-transition: 200ms
}
body:has(.vc-collapsible-ui-transition-260) {
--vc-cui-transition: 260ms
}
body:has(.vc-collapsible-ui-transition-320) {
--vc-cui-transition: 320ms
}
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])) {
position: relative;
z-index: 1;
overflow: hidden !important;
transition: var(--vc-cui-inline-transition) !important
}
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])):is(:hover, :focus-within) {
z-index: 3
}
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"]), :is(:hover, :focus-within), :is(.vc-betterFolders-sidebar-grid:has(.vc-betterFolders-sidebar:is(:hover, :focus-within)) nav[class*="guilds_"])) {
background: var(--background-gradient-low, var(--background-base-lowest)) !important;
border-start-end-radius: var(--radius-sm, 8px);
width: var(--vc-cui-collapsed-size) !important;
inline-size: var(--vc-cui-collapsed-size) !important;
min-width: var(--vc-cui-collapsed-size) !important;
min-inline-size: var(--vc-cui-collapsed-size) !important;
max-width: var(--vc-cui-collapsed-size) !important;
max-inline-size: var(--vc-cui-collapsed-size) !important;
flex: 0 0 var(--vc-cui-collapsed-size) !important
}
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar {
position: relative;
z-index: 1;
overflow: hidden !important;
transition: var(--vc-cui-inline-transition) !important
}
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within)) .vc-betterFolders-sidebar {
z-index: 3;
transition-delay: var(--vc-cui-hover-delay)
}
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:not(:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within))) .vc-betterFolders-sidebar {
width: 0 !important;
inline-size: 0 !important;
min-width: 0 !important;
min-inline-size: 0 !important;
max-width: 0 !important;
max-inline-size: 0 !important;
flex: 0 0 0 !important
}
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebarList_"] {
overflow: hidden !important;
transition: var(--vc-cui-inline-transition) !important
}
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebarList_"]:is(:hover, :focus-within) {
transition-delay: var(--vc-cui-hover-delay)
}
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebarList_"]:is(:hover, :focus-within) > * {
opacity: 1;
pointer-events: auto;
transition: var(--vc-cui-fade-transition);
transition-delay: var(--vc-cui-hover-delay)
}
body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within))) [class*="sidebarList_"]:not(:is(:hover, :focus-within)) {
width: var(--vc-cui-collapsed-size) !important;
inline-size: var(--vc-cui-collapsed-size) !important;
min-width: var(--vc-cui-collapsed-size) !important;
min-inline-size: var(--vc-cui-collapsed-size) !important;
max-width: var(--vc-cui-collapsed-size) !important;
max-inline-size: var(--vc-cui-collapsed-size) !important;
flex: 0 0 var(--vc-cui-collapsed-size) !important
}
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"] {
overflow: hidden !important;
transition: var(--vc-cui-inline-transition) !important
}
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) {
width: var(--vc-cui-user-area-collapsed-width) !important;
inline-size: var(--vc-cui-user-area-collapsed-width) !important;
min-width: var(--vc-cui-user-area-collapsed-width) !important;
min-inline-size: var(--vc-cui-user-area-collapsed-width) !important;
max-width: var(--vc-cui-user-area-collapsed-width) !important;
max-inline-size: var(--vc-cui-user-area-collapsed-width) !important
}
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) .vc-collapsible-ui-user-area-controls {
max-width: 0 !important;
min-width: 0 !important;
overflow: hidden !important;
opacity: 0;
pointer-events: none;
transition:
max-width var(--vc-cui-transition) var(--vc-cui-ease),
opacity var(--vc-cui-transition) var(--vc-cui-ease) !important
}
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within) .vc-collapsible-ui-user-area-controls {
opacity: 1;
pointer-events: auto
}
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"] {
overflow: hidden !important;
transition: var(--vc-cui-inline-transition) !important
}
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"]:is(:hover, :focus-within) {
transition-delay: var(--vc-cui-hover-delay)
}
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"]:is(:hover, :focus-within) > * {
opacity: 1;
pointer-events: auto;
transition: var(--vc-cui-fade-transition);
transition-delay: var(--vc-cui-hover-delay)
}
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"]:not(:is(:hover, :focus-within)) {
width: var(--vc-cui-collapsed-size) !important;
min-width: var(--vc-cui-collapsed-size) !important;
max-width: var(--vc-cui-collapsed-size) !important
}
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"]:not(:is(:hover, :focus-within)) > * {
opacity: 0;
pointer-events: none;
transition: var(--vc-cui-fade-transition)
}
body:has(.vc-collapsible-ui-user-area-collapsed) [class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within) > * {
opacity: 1;
pointer-events: auto;
transition: var(--vc-cui-fade-transition)
}
body:has(.vc-collapsible-ui-user-area-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) {
height: var(--vc-cui-hover-peek) !important;
block-size: var(--vc-cui-hover-peek) !important;
min-height: var(--vc-cui-hover-peek) !important;
min-block-size: var(--vc-cui-hover-peek) !important;
max-height: var(--vc-cui-hover-peek) !important;
max-block-size: var(--vc-cui-hover-peek) !important;
overflow: hidden !important;
border-top-width: 0 !important;
padding-block: 0 !important;
transition: var(--vc-cui-block-transition) !important
}
body:has(.vc-collapsible-ui-user-area-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) > * {
opacity: 0;
pointer-events: none;
transition: var(--vc-cui-fade-transition)
}
body:has(.vc-collapsible-ui-guild-bar-collapsed):has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) {
width: calc(var(--vc-cui-hover-peek) + 4px) !important;
inline-size: calc(var(--vc-cui-hover-peek) + 4px) !important;
min-width: calc(var(--vc-cui-hover-peek) + 4px) !important;
min-inline-size: calc(var(--vc-cui-hover-peek) + 4px) !important;
max-width: calc(var(--vc-cui-hover-peek) + 4px) !important;
max-inline-size: calc(var(--vc-cui-hover-peek) + 4px) !important
}
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"] {
overflow: hidden !important;
transition: var(--vc-cui-block-transition) !important
}
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:is(:hover, :focus-within) {
transition-delay: var(--vc-cui-hover-delay)
}
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:is(:hover, :focus-within) > * {
opacity: 1;
pointer-events: auto;
transition: var(--vc-cui-fade-transition);
transition-delay: var(--vc-cui-hover-delay)
}
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:not(:is(:hover, :focus-within)) {
height: var(--vc-cui-hover-peek) !important;
block-size: var(--vc-cui-hover-peek) !important;
min-height: var(--vc-cui-hover-peek) !important;
min-block-size: var(--vc-cui-hover-peek) !important;
max-height: var(--vc-cui-hover-peek) !important;
max-block-size: var(--vc-cui-hover-peek) !important;
padding-block: 0 !important
}
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:not(:is(:hover, :focus-within)) > * {
opacity: 0;
pointer-events: none;
transition: var(--vc-cui-fade-transition)
}
/* stylelint-disable no-descending-specificity */
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]) {
position: relative;
z-index: 3;
overflow: hidden !important;
transition: var(--vc-cui-block-transition) !important
}
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="base_"]:has(> [class*="bar_"] > [class*="title_"]) {
/* stylelint-disable-next-line value-keyword-case */
grid-template-rows: [top] var(--vc-cui-hover-peek) [titleBarEnd] auto [noticeEnd] minmax(0, 1fr) [end] !important
}
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="base_"]:has(> [class*="bar_"]:is(:hover, :focus-within) > [class*="title_"]) {
/* stylelint-disable-next-line value-keyword-case */
grid-template-rows: [top] var(--vc-cui-header-bar-height) [titleBarEnd] auto [noticeEnd] minmax(0, 1fr) [end] !important
}
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):is(:hover, :focus-within) {
height: var(--vc-cui-header-bar-height) !important;
block-size: var(--vc-cui-header-bar-height) !important;
min-height: var(--vc-cui-header-bar-height) !important;
min-block-size: var(--vc-cui-header-bar-height) !important;
max-height: var(--vc-cui-header-bar-height) !important;
max-block-size: var(--vc-cui-header-bar-height) !important;
transition-delay: var(--vc-cui-hover-delay)
}
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):is(:hover, :focus-within) > * {
opacity: 1;
pointer-events: auto;
transition: var(--vc-cui-fade-transition);
transition-delay: var(--vc-cui-hover-delay)
}
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):not(:is(:hover, :focus-within)) {
height: var(--vc-cui-hover-peek) !important;
block-size: var(--vc-cui-hover-peek) !important;
min-height: var(--vc-cui-hover-peek) !important;
min-block-size: var(--vc-cui-hover-peek) !important;
max-height: var(--vc-cui-hover-peek) !important;
max-block-size: var(--vc-cui-hover-peek) !important;
padding-block: 0 !important
}
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):not(:is(:hover, :focus-within)) > * {
opacity: 0;
pointer-events: none;
transition: var(--vc-cui-fade-transition)
}
/* stylelint-enable no-descending-specificity */
/* stylelint-disable no-descending-specificity */
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within)) .vc-betterFolders-sidebar > *,
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])):is(:hover, :focus-within) > * {
opacity: 1;
pointer-events: auto;
transition: var(--vc-cui-fade-transition);
transition-delay: calc(var(--vc-cui-hover-delay) + var(--vc-cui-transition))
}
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])):is(:hover, :focus-within) > [class*="unreadMentionsIndicator"] {
pointer-events: none
}
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:not(:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within))) .vc-betterFolders-sidebar > *,
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"]), :is(:hover, :focus-within), :is(.vc-betterFolders-sidebar-grid:has(.vc-betterFolders-sidebar:is(:hover, :focus-within)) nav[class*="guilds_"])) > * {
opacity: 0;
pointer-events: none;
transition: var(--vc-cui-fade-transition)
}
body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within))) [class*="sidebarList_"]:not(:is(:hover, :focus-within)) > * {
opacity: 0;
pointer-events: none;
transition: var(--vc-cui-fade-transition)
}
/* stylelint-enable no-descending-specificity */
.vc-collapsible-ui-chat-buttons {
display: flex;
align-items: center;
min-inline-size: 0
}
.vc-collapsible-ui-chat-buttons-collapsed {
min-inline-size: var(--vc-cui-hover-peek)
}
.vc-collapsible-ui-chat-buttons-items {
display: flex;
align-items: center;
min-inline-size: 0;
max-inline-size: var(--vc-cui-chat-buttons-width);
overflow: hidden;
transition:
max-inline-size var(--vc-cui-transition) var(--vc-cui-ease),
opacity var(--vc-cui-transition) var(--vc-cui-ease),
margin var(--vc-cui-transition) var(--vc-cui-ease)
}
.vc-collapsible-ui-chat-buttons-collapsed .vc-collapsible-ui-chat-buttons-items {
max-inline-size: 0;
opacity: 0;
pointer-events: none
}
.vc-collapsible-ui-chat-buttons-collapsed:is(:hover, :focus-within) .vc-collapsible-ui-chat-buttons-items {
max-inline-size: var(--vc-cui-chat-buttons-width);
opacity: 1;
pointer-events: auto;
transition-delay: var(--vc-cui-hover-delay)
}
@media (prefers-reduced-motion: reduce) {
body:has(.vc-collapsible-ui-state) {
--vc-cui-transition: 0ms
}
}

View file

@ -4,11 +4,12 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { Heading } from "@components/Heading";
import { CogWheel } from "@components/Icons"; import { CogWheel } from "@components/Icons";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { type ModalProps,ModalRoot, ModalSize, openModal } from "@utils/modal"; import { RenderModalProps } from "@vencord/discord-types";
import { TextInput, useCallback, useEffect, useMemo, useRef, useState } from "@webpack/common"; import { Modal, openModal, TextInput, useCallback, useEffect, useMemo, useRef, useState } from "@webpack/common";
import { import {
type CommandEntry, getCategoryGroupLabel, getCategoryWeight, getCommandSearchText, getRecentRank, getRegistryVersion, listCommands, subscribeRegistry type CommandEntry, getCategoryGroupLabel, getCategoryWeight, getCommandSearchText, getRecentRank, getRegistryVersion, listCommands, subscribeRegistry
@ -56,7 +57,7 @@ export function openCommandPicker(props: CommandPickerProps) {
)); ));
} }
function CommandPickerModal({ modalProps, commands: providedCommands, allowMultiple = false, initialQuery = "", initialSelectedIds = [], includeHiddenInSearch = false, emptyStateText = "No commands found", filter: filterPredicate, onSelect, onComplete, onClose }: CommandPickerProps & { modalProps: ModalProps; }) { function CommandPickerModal({ modalProps, commands: providedCommands, allowMultiple = false, initialQuery = "", initialSelectedIds = [], includeHiddenInSearch = false, emptyStateText = "No commands found", filter: filterPredicate, onSelect, onComplete, onClose }: CommandPickerProps & { modalProps: RenderModalProps; }) {
const [query, setQuery] = useState(initialQuery); const [query, setQuery] = useState(initialQuery);
const [registryVersion, setRegistryVersion] = useState(() => getRegistryVersion()); const [registryVersion, setRegistryVersion] = useState(() => getRegistryVersion());
const [selectedIndex, setSelectedIndex] = useState(-1); const [selectedIndex, setSelectedIndex] = useState(-1);
@ -64,6 +65,7 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
const [pickedIds, setPickedIds] = useState<string[]>(() => normalizedInitialSelected); const [pickedIds, setPickedIds] = useState<string[]>(() => normalizedInitialSelected);
const itemRefs = useRef<(HTMLButtonElement | null)[]>([]); const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);
const pickedIdsRef = useRef<string[]>([]); const pickedIdsRef = useRef<string[]>([]);
const listRef = useRef<any>(null);
useEffect(() => { useEffect(() => {
if (providedCommands) return; if (providedCommands) return;
@ -142,7 +144,12 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
for (const entry of availableCommands) { for (const entry of availableCommands) {
if (!includeHiddenInSearch && entry.hiddenInSearch) continue; if (!includeHiddenInSearch && entry.hiddenInSearch) continue;
const searchText = getCommandSearchText(entry.id); const searchText = getCommandSearchText(entry.id) || [
entry.label,
entry.description ?? "",
...entry.keywords ?? [],
...entry.tags ?? []
].filter(Boolean).join(" ").toLowerCase();
if (!searchText.includes(lowerQuery)) continue; if (!searchText.includes(lowerQuery)) continue;
const score = scoreCommand(entry); const score = scoreCommand(entry);
@ -203,8 +210,22 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
useEffect(() => { useEffect(() => {
if (selectedIndex < 0) return; if (selectedIndex < 0) return;
const rawContainer = listRef.current as any;
const container = rawContainer?.getScrollerNode ? rawContainer.getScrollerNode() : rawContainer;
const node = itemRefs.current[selectedIndex]; const node = itemRefs.current[selectedIndex];
node?.scrollIntoView({ block: "nearest" }); if (!container || !node) return;
const containerRect = container.getBoundingClientRect();
const nodeRect = node.getBoundingClientRect();
if (nodeRect.bottom > containerRect.bottom) {
container.scrollTop += nodeRect.bottom - containerRect.bottom;
return;
}
if (nodeRect.top < containerRect.top) {
container.scrollTop -= containerRect.top - nodeRect.top;
}
}, [selectedIndex, items]); }, [selectedIndex, items]);
itemRefs.current = itemRefs.current.slice(0, items.length); itemRefs.current = itemRefs.current.slice(0, items.length);
@ -321,17 +342,26 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
}; };
return ( return (
<ModalRoot {...modalProps} size={ModalSize.SMALL} className={classes(cn("vc-command-palette"), cl("picker"))}> <Modal
<div className={classes(cl("shell"), cl("picker-shell"))} onKeyDown={handleKeyDown}> {...modalProps}
<div className={cl("input")}> size="lg"
<TextInput scrollerRef={listRef}
autoFocus title={
value={query} <div className={classes(cl("shell"), cl("picker-shell"), cn("vc-command-palette"), cl("picker"))} onKeyDown={handleKeyDown}>
onChange={setQuery} <Heading tag="h1" className={cl("title")}>Command Palette</Heading>
placeholder="Search commands"
className={cl("main-input")} <div className={cl("input")}>
/> <TextInput
autoFocus
value={query}
onChange={setQuery}
placeholder="Search commands"
/>
</div>
</div> </div>
}
>
<div className={classes(cl("shell"), cl("picker-shell"), cn("vc-command-palette"), cl("picker"))} onKeyDown={handleKeyDown}>
<div className={cl("list")}> <div className={cl("list")}>
{!hasCommands && <div className={cl("empty")}>{emptyStateText}</div>} {!hasCommands && <div className={cl("empty")}>{emptyStateText}</div>}
{items.map((item, index) => { {items.map((item, index) => {
@ -373,6 +403,6 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
})} })}
</div> </div>
</div> </div>
</ModalRoot> </Modal>
); );
} }

View file

@ -230,5 +230,8 @@ export default definePlugin({
}); });
}, },
chatBarButtonWrapper: wrapChatBarChildren, chatBarButtonWrapper: {
wrapper: wrapChatBarChildren,
priority: 0,
},
}); });

View file

@ -20,7 +20,11 @@ export interface RegistryStore {
getTreeCommandCache(): Map<string, CommandEntry[]>; getTreeCommandCache(): Map<string, CommandEntry[]>;
} }
let instance: RegistryStore | null = null;
export function createRegistryStore(): RegistryStore { export function createRegistryStore(): RegistryStore {
if (instance) return instance;
let registryVersion = 0; let registryVersion = 0;
let cachedSortedCommands: CommandEntry[] | null = null; let cachedSortedCommands: CommandEntry[] | null = null;
const categoryCommandCache = new Map<string, CommandEntry[]>(); const categoryCommandCache = new Map<string, CommandEntry[]>();
@ -38,7 +42,7 @@ export function createRegistryStore(): RegistryStore {
for (const listener of registryListeners) listener(registryVersion); for (const listener of registryListeners) listener(registryVersion);
}; };
return { instance = {
getRegistryVersion: () => registryVersion, getRegistryVersion: () => registryVersion,
bumpRegistryVersion: () => { bumpRegistryVersion: () => {
registryVersion += 1; registryVersion += 1;
@ -77,4 +81,6 @@ export function createRegistryStore(): RegistryStore {
getCategoryCommandCache: () => categoryCommandCache, getCategoryCommandCache: () => categoryCommandCache,
getTreeCommandCache: () => treeCommandCache getTreeCommandCache: () => treeCommandCache
}; };
return instance;
} }

View file

@ -6,9 +6,8 @@
/* Command Palette Modal Container */ /* Command Palette Modal Container */
.vc-command-palette { .vc-command-palette {
--cp-bg: #0f0f10; --cp-bg-secondary: transparent;
--cp-bg-secondary: #18181a; --cp-bg-tertiary: transparent;
--cp-bg-tertiary: #1f1f21;
--cp-bg-dropdown: #0b0b0d; --cp-bg-dropdown: #0b0b0d;
--cp-bg-hover: #252528; --cp-bg-hover: #252528;
--cp-bg-selected: #2a2a2e; --cp-bg-selected: #2a2a2e;
@ -26,12 +25,6 @@
--cp-radius-sm: 8px; --cp-radius-sm: 8px;
--cp-shadow: 0 16px 44px rgb(0 0 0 / 38%), 0 6px 18px rgb(0 0 0 / 28%); --cp-shadow: 0 16px 44px rgb(0 0 0 / 38%), 0 6px 18px rgb(0 0 0 / 28%);
width: min(900px, 92vw) !important;
max-width: min(900px, 92vw) !important;
background: var(--cp-bg) !important;
border-radius: var(--cp-radius) !important;
box-shadow: var(--cp-shadow) !important;
border: 1px solid var(--cp-border) !important;
overflow: visible !important; overflow: visible !important;
box-sizing: border-box; box-sizing: border-box;
will-change: opacity, transform; will-change: opacity, transform;
@ -44,21 +37,19 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
max-width: 100%;
min-height: 67vh;
max-height: 67vh;
background: var(--cp-bg);
border-radius: var(--cp-radius);
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
overflow: hidden;
} }
/* Input Area */ /* Input Area */
.vc-command-palette-input { .vc-command-palette-input {
padding: 12px 22px 10px; border-bottom: none;
border-bottom: 1px solid var(--cp-border); position: sticky;
position: relative; top: 0;
z-index: 10;
width: 105%;
box-sizing: border-box;
background: var(--bg-overlay-3, var(--background-secondary));
} }
.vc-command-palette-main-input input { .vc-command-palette-main-input input {
@ -303,21 +294,8 @@
.vc-command-palette-list { .vc-command-palette-list {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
overflow-y: auto;
padding: 0; padding: 0;
} width: 100%;
.vc-command-palette-list::-webkit-scrollbar {
width: 6px;
}
.vc-command-palette-list::-webkit-scrollbar-track {
background: transparent;
}
.vc-command-palette-list::-webkit-scrollbar-thumb {
background: var(--cp-bg-tertiary);
border-radius: 3px;
} }
/* Section Labels */ /* Section Labels */
@ -388,7 +366,7 @@
} }
.vc-command-palette-row-subtitle { .vc-command-palette-row-subtitle {
font-size: 13px; font-size: 14px;
color: var(--cp-text-secondary); color: var(--cp-text-secondary);
line-height: 1.4; line-height: 1.4;
} }
@ -435,22 +413,17 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 10px 20px; padding: 10px 20px;
background: var(--cp-bg);
border-top: 1px solid var(--cp-border);
gap: 16px; gap: 16px;
min-height: 48px; min-height: 48px;
} position: sticky;
bottom: 0;
.vc-command-palette-picker .vc-command-palette-shell { z-index: 10;
max-height: 62vh; background: var(--bg-overlay-3, var(--background-secondary));
} }
.vc-command-palette-picker .vc-command-palette-input { .vc-command-palette-picker .vc-command-palette-input {
padding: 12px 16px 10px; width: 105%;
} height: 100%;
.vc-command-palette-picker .vc-command-palette-list {
max-height: 54vh;
} }
.vc-command-palette-action-bar-action { .vc-command-palette-action-bar-action {
@ -520,11 +493,13 @@
.vc-command-palette-calculator { .vc-command-palette-calculator {
padding: 12px 16px; padding: 12px 16px;
background: transparent; background: transparent;
border-bottom: 1px solid var(--cp-border); border-bottom: none;
} }
.vc-command-palette-calculator-title { .vc-command-palette-calculator-title {
font-size: 13px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--cp-text-secondary); color: var(--cp-text-secondary);
margin: 0 0 10px; margin: 0 0 10px;
@ -749,46 +724,7 @@
font-size: 13px; font-size: 13px;
} }
/* Compact Mode */
.vc-command-palette-compact .vc-command-palette-shell {
max-height: fit-content;
min-height: 100%;
overflow: hidden;
}
.vc-command-palette-compact {
min-height: 0 !important;
}
.vc-command-palette-action-bar-logo {
display: flex;
align-items: center;
color: var(--cp-text-muted);
}
.vc-command-palette-compact .vc-command-palette-input {
border-bottom: none;
}
.vc-command-palette-action-bar-compact-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 12px;
background: var(--cp-bg-secondary);
border: 1px solid var(--cp-border);
border-radius: 8px;
color: var(--cp-text-secondary);
font-size: 12px;
cursor: pointer;
transition: all 0.15s ease;
line-height: 1;
}
.vc-command-palette-action-bar-compact-btn:hover {
background: var(--cp-bg-hover);
border-color: var(--cp-border-strong);
}
.vc-command-palette-keybind-input { .vc-command-palette-keybind-input {
display: flex; display: flex;
@ -1029,7 +965,7 @@
} }
.vc-command-palette-picker .vc-command-palette-main-input input { .vc-command-palette-picker .vc-command-palette-main-input input {
min-height: 40px !important; width: 100px !important;
height: 40px !important; height: 40px !important;
font-size: 18px !important; font-size: 18px !important;
} }
@ -1052,6 +988,11 @@
outline: none !important; outline: none !important;
} }
.vc-command-palette-main-search-input {
width: 100%;
margin-left: 6px;
}
input.vc-command-palette-main-search-input:read-only, input.vc-command-palette-main-search-input:read-only,
.vc-command-palette-main-search-input input:read-only { .vc-command-palette-main-search-input input:read-only {
color: var(--cp-text-muted) !important; color: var(--cp-text-muted) !important;

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { closeModal, openModal } from "@utils/modal"; import { closeModal, openModal } from "@webpack/common";
import { CommandPaletteModal } from "./ui/CommandPaletteModal"; import { CommandPaletteModal } from "./ui/CommandPaletteModal";

View file

@ -4,40 +4,33 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { EquicordIcon } from "@equicordplugins/discordDevBanner/components";
import { IS_MAC } from "@utils/constants"; import { IS_MAC } from "@utils/constants";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc"; import { SVGProps } from "react";
const cl = classNameFactory("vc-command-palette-"); const cl = classNameFactory("vc-command-palette-");
interface CommandPaletteActionBarProps { interface CommandPaletteActionBarProps {
selectedLabel?: string; selectedLabel?: string;
onOpenActions?(): void; onOpenActions?(): void;
compact?: boolean;
onExpand?(): void;
} }
export function CommandPaletteActionBar({ selectedLabel, onOpenActions, compact, onExpand }: CommandPaletteActionBarProps) { const EquicordIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
const actionsShortcutLabel = IS_MAC ? "⌘L" : "Ctrl+L"; <svg
{...props}
fill="currentColor"
viewBox="0 0 930 930"
width="32px"
height="32px"
>
<path d="M836 465.5C836 670.121 670.121 836 465.5 836C260.879 836 95 670.121 95 465.5C95 260.879 260.879 95 465.5 95C670.121 95 836 260.879 836 465.5ZM242.322 465.5C242.322 588.758 342.242 688.678 465.5 688.678C588.758 688.678 688.678 588.758 688.678 465.5C688.678 342.242 588.758 242.322 465.5 242.322C342.242 242.322 242.322 342.242 242.322 465.5Z" />
<path d="M584.219 465.898C584.219 531.245 531.245 584.219 465.898 584.219C440.35 584.219 416.693 576.122 397.353 562.354L260.937 644.321C451.4 528.542 329.698 492.311 204.538 566.663L348.433 480.202C347.868 475.513 347.577 470.74 347.577 465.898C347.577 400.552 400.552 347.577 465.898 347.577C491.108 347.577 514.477 355.462 533.673 368.899L627.819 312.331C610.898 294.399 591.056 279.324 569.045 267.796C534.72 249.819 496.306 241.088 457.582 242.462C418.858 243.837 381.16 255.27 348.196 275.637C315.232 296.003 288.138 324.6 269.581 358.616C262.856 370.943 257.336 383.828 253.065 397.091C240.595 435.815 209.386 470.244 168.712 470.997C128.037 471.751 93.7099 439.084 101.005 399.061C108.06 360.359 121.262 322.87 140.254 288.06C171.06 231.591 216.039 184.116 270.763 150.306C325.486 116.495 388.07 97.5155 452.356 95.2335C516.641 92.9515 580.413 107.446 637.397 137.29C694.38 167.134 742.612 211.301 777.345 265.444C812.079 319.586 832.118 381.839 835.491 446.076C838.863 510.313 825.452 574.322 796.579 631.804C778.78 667.239 755.483 699.439 727.687 727.279C698.944 756.068 652.543 746.455 629.998 712.591C607.453 678.727 617.982 633.466 642.711 601.164C651.181 590.1 658.628 578.224 664.932 565.676C682.324 531.051 690.402 492.494 688.371 453.799C687.303 433.462 683.462 413.454 677.02 394.312L583.246 450.657C583.889 455.647 584.219 460.734 584.219 465.898ZM260.937 644.321C258.599 645.742 256.214 647.175 253.783 648.619L260.937 644.321Z" />
<path d="M470.711 406.73C493.342 393.132 522.712 400.455 536.311 423.086C549.909 445.718 542.587 475.088 519.955 488.687L253.783 648.619L204.538 566.663L470.711 406.73Z" />
</svg>
);
if (compact) { export function CommandPaletteActionBar({ selectedLabel, onOpenActions }: CommandPaletteActionBarProps) {
return ( const actionsShortcutLabel = IS_MAC ? "⌘L" : "Ctrl+L";
<div className={cl("action-bar")}>
<div className={classes(cl("action-bar-label"), cl("action-bar-logo"))}>
<EquicordIcon />
</div>
<button
type="button"
className={cl("action-bar-compact-btn")}
onClick={onExpand}
>
<span className={cl("action-bar-actions-label")}>Show More</span>
<span>↓</span>
</button>
</div>
);
}
return ( return (
<div className={cl("action-bar")}> <div className={cl("action-bar")}>

View file

@ -6,13 +6,14 @@
import "../style.css"; import "../style.css";
import { Heading } from "@components/Heading";
import { IS_MAC } from "@utils/constants"; import { IS_MAC } from "@utils/constants";
import { classNameFactory, classNameToSelector } from "@utils/css"; import { classNameFactory, classNameToSelector } from "@utils/css";
import { copyWithToast } from "@utils/discord"; import { copyWithToast } from "@utils/discord";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { type ModalProps, ModalRoot, ModalSize } from "@utils/modal"; import { RenderModalProps } from "@vencord/discord-types";
import { TextInput, Toasts, useEffect, useMemo, useRef, useState } from "@webpack/common"; import { Modal, TextInput, Toasts, useEffect, useMemo, useRef, useState } from "@webpack/common";
import type { KeyboardEvent as ReactKeyboardEvent } from "react"; import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { buildQueryResolution } from "../actions/executors"; import { buildQueryResolution } from "../actions/executors";
@ -282,20 +283,18 @@ function createInitialPageState(ref: PalettePageRef): PalettePageValuesState {
}; };
} }
export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: ModalProps; instanceKey: number; }) { export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: RenderModalProps; instanceKey: number; }) {
const { const {
compactStartEnabled = true,
closeAfterExecute = true closeAfterExecute = true
} = settings.use(); } = settings.use();
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [expanded, setExpanded] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(-1); const [selectedIndex, setSelectedIndex] = useState(-1);
const [selectedKey, setSelectedKey] = useState<string | null>(null); const [selectedKey, setSelectedKey] = useState<string | null>(null);
const [keyboardSelectedKey, setKeyboardSelectedKey] = useState<string | null>(null); const [keyboardSelectedKey, setKeyboardSelectedKey] = useState<string | null>(null);
const [registryVersion, setRegistryVersion] = useState(() => getRegistryVersion()); const [registryVersion, setRegistryVersion] = useState(() => getRegistryVersion());
const [pinnedIds, setPinnedIds] = useState<Set<string>>(new Set()); const [pinnedIds, setPinnedIds] = useState<Set<string>>(new Set());
const [navigationLevel, setNavigationLevel] = useState<NavigationLevel>(() => { const [navigationLevel, setNavigationLevel] = useState<NavigationLevel>(() => {
if (compactStartEnabled || !persistedCategoryId) return { type: "root" }; if (!persistedCategoryId) return { type: "root" };
return buildNavigationLevelForCategory(persistedCategoryId); return buildNavigationLevelForCategory(persistedCategoryId);
}); });
const [activePromptCommand, setActivePromptCommand] = useState<CommandEntry | null>(null); const [activePromptCommand, setActivePromptCommand] = useState<CommandEntry | null>(null);
@ -326,7 +325,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
useEffect(() => { useEffect(() => {
setQuery(""); setQuery("");
setExpanded(false);
setSelectedIndex(-1); setSelectedIndex(-1);
setSelectedKey(null); setSelectedKey(null);
setKeyboardSelectedKey(null); setKeyboardSelectedKey(null);
@ -343,38 +341,22 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
closeReasonRef.current = null; closeReasonRef.current = null;
keyboardNavigationAtRef.current = 0; keyboardNavigationAtRef.current = 0;
setNavigationLevel(() => { setNavigationLevel(() => {
if (compactStartEnabled || !persistedCategoryId) return { type: "root" }; if (!persistedCategoryId) return { type: "root" };
return buildNavigationLevelForCategory(persistedCategoryId); return buildNavigationLevelForCategory(persistedCategoryId);
}); });
}, [compactStartEnabled, instanceKey]); }, [instanceKey]);
useEffect(() => { useEffect(() => {
return () => { return () => {
if (closeReasonRef.current === "explicit-root") {
clearPersistedNavigation();
return;
}
if (closeReasonRef.current === "programmatic") {
clearPersistedNavigation();
return;
}
if (navigationLevel.type === "category") {
persistedCategoryId = navigationLevel.categoryId;
return;
}
clearPersistedNavigation(); clearPersistedNavigation();
}; };
}, [navigationLevel]); }, []);
useEffect(() => { useEffect(() => {
setIsActionsMenuOpen(false); setIsActionsMenuOpen(false);
}, [navigationLevel]); }, [navigationLevel]);
const trimmedQuery = query.trim(); const trimmedQuery = query.trim();
const compact = compactStartEnabled && trimmedQuery.length === 0 && !expanded && navigationLevel.type === "root" && pageStack.length === 0;
const allCommands = useMemo(() => listCommands(), [registryVersion]); const allCommands = useMemo(() => listCommands(), [registryVersion]);
@ -472,7 +454,7 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
expandedItems.push({ type: "section", id: "section-breadcrumb", label: breadcrumb }); expandedItems.push({ type: "section", id: "section-breadcrumb", label: breadcrumb });
} }
if (!compact && navigationLevel.type === "root" && trimmedQuery.length === 0) { if (navigationLevel.type === "root" && trimmedQuery.length === 0) {
if (pinnedCandidates.length > 0) { if (pinnedCandidates.length > 0) {
expandedItems.push({ type: "section", id: "section-pinned", label: "Pinned" }); expandedItems.push({ type: "section", id: "section-pinned", label: "Pinned" });
expandedItems.push(...pinnedCandidates); expandedItems.push(...pinnedCandidates);
@ -507,7 +489,7 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
} }
return expandedItems; return expandedItems;
}, [compact, navigationLevel, pinnedCandidates, rankedCommandCandidates, recentCandidates, suggestedCandidates, trimmedQuery]); }, [navigationLevel, pinnedCandidates, rankedCommandCandidates, recentCandidates, suggestedCandidates, trimmedQuery]);
const emptyStateText = navigationLevel.type === "category" && navigationLevel.categoryId === MENTIONS_CATEGORY_ID const emptyStateText = navigationLevel.type === "category" && navigationLevel.categoryId === MENTIONS_CATEGORY_ID
? "All caught up." ? "All caught up."
@ -606,12 +588,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
}, [queryCandidates, selectedPromptCandidateId]); }, [queryCandidates, selectedPromptCandidateId]);
useEffect(() => { useEffect(() => {
if (compact) {
setSelectedIndex(-1);
setSelectedKey(null);
return;
}
if (!hasCommandItems) { if (!hasCommandItems) {
setSelectedIndex(-1); setSelectedIndex(-1);
setSelectedKey(null); setSelectedKey(null);
@ -635,12 +611,13 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
setSelectedIndex(firstSelectable); setSelectedIndex(firstSelectable);
setSelectedKey(items[firstSelectable].id); setSelectedKey(items[firstSelectable].id);
}, [hasCommandItems, items, selectedKey, compact]); }, [hasCommandItems, items, selectedKey]);
useEffect(() => { useEffect(() => {
if (selectedIndex < 0) return; if (selectedIndex < 0) return;
if (selectionSource !== "keyboard") return; if (selectionSource !== "keyboard") return;
const container = listRef.current; const rawContainer = listRef.current as any;
const container = rawContainer?.getScrollerNode ? rawContainer.getScrollerNode() : rawContainer;
const node = itemRefs.current[selectedIndex]; const node = itemRefs.current[selectedIndex];
if (!container || !node) return; if (!container || !node) return;
@ -662,16 +639,18 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
if (previous === trimmedQuery) return; if (previous === trimmedQuery) return;
lastTrimmedQueryRef.current = trimmedQuery; lastTrimmedQueryRef.current = trimmedQuery;
if (compact || isPageOpen) return; if (isPageOpen) return;
setSelectedIndex(-1); setSelectedIndex(-1);
setSelectedKey(null); setSelectedKey(null);
setKeyboardSelectedKey(null); setKeyboardSelectedKey(null);
if (listRef.current) { const rawContainer = listRef.current as any;
listRef.current.scrollTop = 0; const container = rawContainer?.getScrollerNode ? rawContainer.getScrollerNode() : rawContainer;
if (container) {
container.scrollTop = 0;
} }
}, [compact, isPageOpen, trimmedQuery]); }, [isPageOpen, trimmedQuery]);
useEffect(() => { useEffect(() => {
if (!focusPromptInput) return; if (!focusPromptInput) return;
@ -768,7 +747,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
setSelectedKey(null); setSelectedKey(null);
setKeyboardSelectedKey(null); setKeyboardSelectedKey(null);
setQuery(""); setQuery("");
setExpanded(false);
}; };
const setActivePageValue = (fieldKey: string, value: string) => { const setActivePageValue = (fieldKey: string, value: string) => {
@ -883,7 +861,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
setSelectedKey(null); setSelectedKey(null);
setKeyboardSelectedKey(null); setKeyboardSelectedKey(null);
setQuery(""); setQuery("");
setExpanded(false);
return true; return true;
}; };
@ -982,7 +959,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
setSelectedKey(null); setSelectedKey(null);
setKeyboardSelectedKey(null); setKeyboardSelectedKey(null);
setQuery(""); setQuery("");
setExpanded(false);
}; };
const updateSelection = (index: number, item: PaletteCandidate) => { const updateSelection = (index: number, item: PaletteCandidate) => {
@ -1055,7 +1031,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
clearPromptState(); clearPromptState();
setSelectedKey(null); setSelectedKey(null);
setQuery(""); setQuery("");
setExpanded(false);
}; };
const onKeyDown = async (event: ReactKeyboardEvent<HTMLDivElement>) => { const onKeyDown = async (event: ReactKeyboardEvent<HTMLDivElement>) => {
@ -1256,17 +1231,12 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
if (isMainInputTarget && event.key === "ArrowDown") { if (isMainInputTarget && event.key === "ArrowDown") {
event.preventDefault(); event.preventDefault();
if (compact) {
setExpanded(true);
return;
}
moveSelection(1); moveSelection(1);
return; return;
} }
if (isMainInputTarget && event.key === "ArrowUp") { if (isMainInputTarget && event.key === "ArrowUp") {
event.preventDefault(); event.preventDefault();
if (compact) return;
if (selectedIndex <= 0) { if (selectedIndex <= 0) {
focusSearchInput(); focusSearchInput();
return; return;
@ -1289,24 +1259,12 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
if (event.key === "ArrowDown") { if (event.key === "ArrowDown") {
event.preventDefault(); event.preventDefault();
if (compact) {
setExpanded(true);
return;
}
moveSelection(1); moveSelection(1);
return; return;
} }
if (event.key === "ArrowUp") { if (event.key === "ArrowUp") {
event.preventDefault(); event.preventDefault();
if (compact) return;
if (selectedIndex <= 0 && trimmedQuery.length === 0 && navigationLevel.type === "root") {
setExpanded(false);
return;
}
moveSelection(-1); moveSelection(-1);
return; return;
} }
@ -1368,14 +1326,8 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
return; return;
} }
if (!compact && trimmedQuery.length > 0) { if (trimmedQuery.length > 0) {
setQuery(""); setQuery("");
setExpanded(false);
return;
}
if (!compact) {
setExpanded(false);
return; return;
} }
@ -1469,124 +1421,126 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
}, [activePage, activePageFieldKey, activePageSpec, activePageState, activePageSuggestions]); }, [activePage, activePageFieldKey, activePageSpec, activePageState, activePageSuggestions]);
return ( return (
<ModalRoot <Modal
{...modalProps} {...modalProps}
className={cn("vc-command-palette", compact && cl("compact"))} size="lg"
size={compact ? ModalSize.SMALL : ModalSize.LARGE} scrollerRef={listRef}
> title={
<div className={cl("shell")} onKeyDown={onKeyDown}> <div className={cn("vc-command-palette", cl("shell"))} onKeyDown={onKeyDown}>
{!isPageOpen && ( <Heading tag="h1" className={cl("title")}>Command Palette</Heading>
<CommandPaletteInput
inputRef={mainInputRef}
autoFocus={!activePromptCommand}
value={query}
onChange={value => {
if (activePromptCommand && value.length > 0) {
clearPromptState();
}
const next = value.trim(); {!isPageOpen && (
setQuery(value); <CommandPaletteInput
inputRef={mainInputRef}
if (next.length > 0) { autoFocus={!activePromptCommand}
setExpanded(true); value={query}
} else { onChange={value => {
if (navigationLevel.type !== "root") { if (activePromptCommand && value.length > 0) {
goBack(); clearPromptState();
return;
} }
setExpanded(false);
} const next = value.trim();
}} setQuery(value);
placeholder={mainInputPlaceholder}
readOnly={false} if (next.length === 0) {
> if (navigationLevel.type !== "root") {
{shouldRenderPromptPreview && promptCommand && ( goBack();
<div return;
ref={promptContainerRef} }
className={classes(cl("query-preview"), !shouldShowPromptCommandLabel && cl("query-preview-without-label"))} }
> }}
{shouldShowPromptCommandLabel && ( placeholder={mainInputPlaceholder}
<span className={cl("query-preview-command-label")}>{promptCommand.label}</span> readOnly={false}
)} >
<div className={cl("query-preview-prompt")} style={queryPreviewPromptStyle}> {shouldRenderPromptPreview && promptCommand && (
{activePromptCommand ? ( <div
<div ref={promptContainerRef}
className={classes(cl("query-prompt-capsule"), cl("query-prompt-capsule-active"))} className={classes(cl("query-preview"), !shouldShowPromptCommandLabel && cl("query-preview-without-label"))}
style={promptChipStyle} >
onMouseDown={() => setShowPromptDropdown(true)} {shouldShowPromptCommandLabel && (
> <span className={cl("query-preview-command-label")}>{promptCommand.label}</span>
<TextInput )}
ref={promptInputRef} <div className={cl("query-preview-prompt")} style={queryPreviewPromptStyle}>
className={cl("prompt-active-input")} {activePromptCommand ? (
value={promptInputValue} <div
onChange={value => { className={classes(cl("query-prompt-capsule"), cl("query-prompt-capsule-active"))}
setPromptInputValue(value); style={promptChipStyle}
setShowPromptDropdown(true); onMouseDown={() => setShowPromptDropdown(true)}
if (selectedPromptCandidateId) { >
setSelectedPromptCandidateId(null); <TextInput
} ref={promptInputRef}
className={cl("prompt-active-input")}
value={promptInputValue}
onChange={value => {
setPromptInputValue(value);
setShowPromptDropdown(true);
if (selectedPromptCandidateId) {
setSelectedPromptCandidateId(null);
}
}}
placeholder={selectedPromptCandidate ? "" : (promptCommand.queryPlaceholder ?? "Action")}
onFocus={() => setShowPromptDropdown(true)}
onBlur={() => {
window.setTimeout(() => {
const active = document.activeElement;
if (promptContainerRef.current?.contains(active)) return;
setShowPromptDropdown(false);
if (selectedPromptCandidateId || promptInputValue.trim().length > 0) return;
clearPromptState();
}, 0);
}}
/>
</div>
) : (
<button
type="button"
className={cl("query-prompt-capsule")}
style={promptChipStyle}
onMouseDown={event => event.preventDefault()}
onClick={() => activatePromptCommand(promptCommand, true)}
>
<span className={cl("query-prompt-capsule-label")}>{promptCommand.queryPlaceholder ?? "Action"}</span>
</button>
)}
{shouldShowPromptDropdown && (
<PaletteDropdown
className={cl("query-preview-dropdown")}
suggestions={promptDropdownSuggestions}
highlightedIndex={promptSelectedSuggestionIndex}
onHover={index => {
const candidate = promptDropdownSuggestions[index];
if (!candidate) return;
setSelectedPromptCandidateId(candidate.id);
}} }}
placeholder={selectedPromptCandidate ? "" : (promptCommand.queryPlaceholder ?? "Action")} onPick={candidate => {
onFocus={() => setShowPromptDropdown(true)} setSelectedPromptCandidateId(candidate.id);
onBlur={() => { if (activePromptIsSingleSelect) {
window.setTimeout(() => { setPromptInputValue(candidate.label);
const active = document.activeElement; } else {
if (promptContainerRef.current?.contains(active)) return; setPromptInputValue("");
}
if (activePromptIsSingleSelect) {
setShowPromptDropdown(false); setShowPromptDropdown(false);
if (selectedPromptCandidateId || promptInputValue.trim().length > 0) return; }
clearPromptState(); setFocusPromptInput(true);
}, 0);
}} }}
/> />
</div> )}
) : ( </div>
<button
type="button"
className={cl("query-prompt-capsule")}
style={promptChipStyle}
onMouseDown={event => event.preventDefault()}
onClick={() => activatePromptCommand(promptCommand, true)}
>
<span className={cl("query-prompt-capsule-label")}>{promptCommand.queryPlaceholder ?? "Action"}</span>
</button>
)}
{shouldShowPromptDropdown && (
<PaletteDropdown
className={cl("query-preview-dropdown")}
suggestions={promptDropdownSuggestions}
highlightedIndex={promptSelectedSuggestionIndex}
onHover={index => {
const candidate = promptDropdownSuggestions[index];
if (!candidate) return;
setSelectedPromptCandidateId(candidate.id);
}}
onPick={candidate => {
setSelectedPromptCandidateId(candidate.id);
if (activePromptIsSingleSelect) {
setPromptInputValue(candidate.label);
} else {
setPromptInputValue("");
}
if (activePromptIsSingleSelect) {
setShowPromptDropdown(false);
}
setFocusPromptInput(true);
}}
/>
)}
</div> </div>
</div> )}
)} </CommandPaletteInput>
</CommandPaletteInput> )}
)}
{!compact && !isPageOpen && calculatorResult && ( {!isPageOpen && calculatorResult && (
<CommandPaletteCalculatorCards result={calculatorResult} mode={calculatorViewMode} /> <CommandPaletteCalculatorCards result={calculatorResult} mode={calculatorViewMode} />
)} )}
</div>
{!compact && !isPageOpen && ( }
<div ref={listRef} className={cl("list")}> >
<div className={cn("vc-command-palette", cl("shell"))} onKeyDown={onKeyDown}>
{!isPageOpen && (
<div className={cl("list")}>
{!hasCommandItems && <div className={cl("empty")}>{emptyStateText}</div>} {!hasCommandItems && <div className={cl("empty")}>{emptyStateText}</div>}
{items.map((item, index) => { {items.map((item, index) => {
if (item.type === "section") { if (item.type === "section") {
@ -1626,9 +1580,9 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
</div> </div>
)} )}
{!compact && isPageOpen && activePageView} {isPageOpen && activePageView}
{!compact && (isActionsMenuOpen || isActionsMenuClosing) && ( {(isActionsMenuOpen || isActionsMenuClosing) && (
<CommandPaletteActionsMenu <CommandPaletteActionsMenu
actions={paletteActions} actions={paletteActions}
title={activePageSpec?.title ?? selectedLabel ?? "Actions"} title={activePageSpec?.title ?? selectedLabel ?? "Actions"}
@ -1651,11 +1605,9 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
setIsActionsMenuOpen(true); setIsActionsMenuOpen(true);
setIsActionsMenuClosing(false); setIsActionsMenuClosing(false);
}} }}
compact={compact}
onExpand={() => setExpanded(true)}
/> />
)} )}
</div> </div>
</ModalRoot> </Modal>
); );
} }

View file

@ -4,8 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { closeModal, ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal"; import { Button, closeModal, Menu, Modal,openModalLazy, Slider, TextInput, useState } from "@webpack/common";
import { Button, Menu, Slider, TextInput, useState } from "@webpack/common";
import { folderIconsData, settings } from "./settings"; import { folderIconsData, settings } from "./settings";
import { folderProp, int2rgba, setFolderData } from "./util"; import { folderProp, int2rgba, setFolderData } from "./util";
@ -95,17 +94,12 @@ export function makeContextItem(a: folderProp) {
action={() => { action={() => {
openModalLazy(async () => { openModalLazy(async () => {
return props => ( return props => (
<ModalRoot {...props}> <Modal
<ModalHeader > {...props}
<div style={{ size="sm"
color: "white" title="Set a New Icon."
}}> >
Set a New Icon. <ImageModal folderId={a.folderId} folderColor={a.folderColor} />
</div>
</ModalHeader>
<ModalContent>
<ImageModal folderId={a.folderId} folderColor={a.folderColor} />
</ModalContent>
<div style={{ <div style={{
color: "white", color: "white",
margin: "2.5%", margin: "2.5%",
@ -113,7 +107,7 @@ export function makeContextItem(a: folderProp) {
}}> }}>
You might have to hover the folder after setting in order for it to refresh. You might have to hover the folder after setting in order for it to refresh.
</div> </div>
</ModalRoot> </Modal>
); );
}, },
{ {

View file

@ -5,17 +5,17 @@
*/ */
import { set } from "@api/DataStore"; import { set } from "@api/DataStore";
import { HeadingPrimary, HeadingSecondary } from "@components/Heading"; import { HeadingSecondary } from "@components/Heading";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; import { RenderModalProps } from "@vencord/discord-types";
import { Button, ColorPicker, useState } from "@webpack/common"; import { ColorPicker, Modal, React, useState } from "@webpack/common";
import { colors, DATASTORE_KEY } from "./index"; import { colors, DATASTORE_KEY } from "./index";
const cl = classNameFactory("vc-customColors-"); const cl = classNameFactory("vc-customColors-");
export function SetColorModal({ id, modalProps }: { id: string, modalProps: ModalProps; }) { export function SetColorModal({ id, modalProps }: { id: string, modalProps: RenderModalProps; }) {
const initialColor = parseInt(colors[id], 16) || 372735; const initialColor = parseInt(colors[id], 16) || 372735;
// color picker default to current color set for user (if null it's 0x05afff :3 ) // color picker default to current color set for user (if null it's 0x05afff :3 )
@ -26,7 +26,7 @@ export function SetColorModal({ id, modalProps }: { id: string, modalProps: Moda
setColorPickerColor(color); setColorPickerColor(color);
} }
function handleKey(e: KeyboardEvent) { function handleKey(e: React.KeyboardEvent) {
if (e.key === "Enter") if (e.key === "Enter")
saveUserColor(); saveUserColor();
} }
@ -44,14 +44,24 @@ export function SetColorModal({ id, modalProps }: { id: string, modalProps: Moda
} }
return ( return (
<ModalRoot {...modalProps}> <Modal
<ModalHeader className={cl("modal-header")}> {...modalProps}
<HeadingPrimary> size="sm"
Custom Color title="Custom Color"
</HeadingPrimary> actions={[
<ModalCloseButton onClick={modalProps.onClose} /> {
</ModalHeader> text: "Save",
<ModalContent className={cl("modal-content")} onKeyDown={handleKey}> variant: "primary",
onClick: saveUserColor
},
{
text: "Delete Entry",
variant: "dangerPrimary",
onClick: deleteUserColor
}
]}
>
<div onKeyDown={handleKey} className={cl("modal-content")}>
<section className={Margins.bottom16}> <section className={Margins.bottom16}>
<HeadingSecondary> <HeadingSecondary>
Pick a Color Pick a Color
@ -62,22 +72,7 @@ export function SetColorModal({ id, modalProps }: { id: string, modalProps: Moda
showEyeDropper={false} showEyeDropper={false}
/> />
</section> </section>
</ModalContent> </div>
</Modal>
<ModalFooter className={cl("modal-footer")}>
<Button
color={Button.Colors.RED}
onClick={deleteUserColor}
>
Delete Entry
</Button>
<Button
color={Button.Colors.BRAND}
onClick={saveUserColor}
>
Save
</Button>
</ModalFooter>
</ModalRoot>
); );
} }

View file

@ -10,12 +10,11 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { get } from "@api/DataStore"; import { get } from "@api/DataStore";
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants"; import { EquicordDevs } from "@utils/constants";
import { openModal } from "@utils/modal";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Channel, User } from "@vencord/discord-types"; import { Channel, User } from "@vencord/discord-types";
import { extractAndLoadChunksLazy } from "@webpack"; import { extractAndLoadChunksLazy } from "@webpack";
import { ChannelStore, Menu, SelectedChannelStore } from "@webpack/common"; import { ChannelStore, Menu, openModal,SelectedChannelStore } from "@webpack/common";
import { SetColorModal } from "./SetColorModal"; import { SetColorModal } from "./SetColorModal";

View file

@ -7,7 +7,7 @@
import "./style.css"; import "./style.css";
import { isPluginEnabled } from "@api/PluginManager"; import { isPluginEnabled } from "@api/PluginManager";
import { definePluginSettings, migrateOldSettingToNewPlugin, migratePluginSetting, migratePluginSettings } from "@api/Settings"; import { definePluginSettings, migratePluginSetting } from "@api/Settings";
import { Divider } from "@components/Divider"; import { Divider } from "@components/Divider";
import { HeadingSecondary } from "@components/Heading"; import { HeadingSecondary } from "@components/Heading";
import { Notice } from "@components/Notice"; import { Notice } from "@components/Notice";
@ -18,21 +18,8 @@ import { t, Translate } from "@utils/translation";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Alerts } from "@webpack/common"; import { Alerts } from "@webpack/common";
migratePluginSettings("Declutter", "BetterUserArea", "Anammox"); migratePluginSetting("Declutter", "removeShopAboveDms", "removeShopAboveDM");
migrateOldSettingToNewPlugin("Declutter", "removeClanTag", "GuildTagSettings", "hideTags"); migratePluginSetting("Declutter", "removeQuestsAboveDms", "removeQuestsAboveDM");
const migrationsAnammox = [
["dms", "removeShopAboveDM"],
["quests", "removeQuestsAboveDM"],
["serverBoost", "removeServerBoostInfo"],
["billing", "removeBillingSettings"],
["gift", "removeGiftButton"],
["emojiList", "removeUnavailableEmojiPicker"],
];
for (const [oldKey, newKey] of migrationsAnammox) {
migratePluginSetting("Declutter", newKey, oldKey);
}
const cl = classNameFactory("vc-declutter-"); const cl = classNameFactory("vc-declutter-");
@ -86,13 +73,13 @@ export const settings = definePluginSettings({
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
component: () => SectionSeparator(t("equicord.declutter.headers.friendsList")), component: () => SectionSeparator(t("equicord.declutter.headers.friendsList")),
}, },
removeShopAboveDM: { removeShopAboveDms: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: t("equicord.declutter.settings.removeShopAboveDM"), description: t("equicord.declutter.settings.removeShopAboveDM"),
default: false, default: false,
restartNeeded: true, restartNeeded: true,
}, },
removeQuestsAboveDM: { removeQuestsAboveDms: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: t("equicord.declutter.settings.removeQuestsAboveDM"), description: t("equicord.declutter.settings.removeQuestsAboveDM"),
default: false, default: false,
@ -179,6 +166,15 @@ export default definePlugin({
}, },
predicate: () => settings.store.removeAvatarDecoration && !isPluginEnabled(decor.name), predicate: () => settings.store.removeAvatarDecoration && !isPluginEnabled(decor.name),
}, },
{
// Avatar decoration on dms list
find: "showCommunicationDisabledStyles",
replacement: {
match: /null==\i\|\|\i\?null:\(0,\i\.jsxs?\)\("img",\{className:\i\.\i,src:\i,alt:" ","aria-hidden":!0\}\)/,
replace: "null"
},
predicate: () => settings.store.removeAvatarDecoration && !isPluginEnabled(decor.name),
},
{ {
// Nameplate // Nameplate
find: ".MINI_PREVIEW,[", find: ".MINI_PREVIEW,[",
@ -279,18 +275,18 @@ export default definePlugin({
{ {
match: /"nitro-tab-group"\)/, match: /"nitro-tab-group"\)/,
replace: "$&&&undefined", replace: "$&&&undefined",
predicate: () => settings.store.removeShopAboveDM, predicate: () => settings.store.removeShopAboveDms,
}, },
{ {
match: /"discord-shop"\)/, match: /NAVIGATION_LINK\}\}\},"discord-shop"\)/,
replace: "$&&&undefined", replace: "$&&&undefined",
predicate: () => settings.store.removeShopAboveDM predicate: () => settings.store.removeShopAboveDms
}, },
{ {
match: /"quests"\)/, match: /\.QUEST_HOME\},"quests"\)/,
replace: "$&&&undefined", replace: "$&&&undefined",
predicate: () => settings.store.removeQuestsAboveDM predicate: () => settings.store.removeQuestsAboveDms
}, },
], ],
}, },
@ -307,7 +303,7 @@ export default definePlugin({
replace: "/*$&*/", replace: "/*$&*/",
}, },
], ],
predicate: () => settings.store.removeShopAboveDM, predicate: () => settings.store.removeShopAboveDms,
}, },
{ {
// Channel list server boost progress bar // Channel list server boost progress bar

View file

@ -0,0 +1,101 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CloudDownloadIcon } from "@components/Icons";
import { EquicordDevs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { pluralise } from "@utils/misc";
import definePlugin from "@utils/types";
import { Message, MessageAttachment } from "@vencord/discord-types";
import { ChannelStore, showToast, Toasts } from "@webpack/common";
const logger = new Logger("DownloadAllAttachments");
async function downloadAll(attachments: MessageAttachment[]) {
let dir: FileSystemDirectoryHandle;
try {
dir = await window.showDirectoryPicker({ mode: "readwrite" });
} catch (e) {
if (e instanceof DOMException && e.name === "AbortError") return;
logger.error("Failed to open directory picker:", e);
return;
}
const usedNames = new Map<string, number>();
function uniqueName(original: string): string {
const count = usedNames.get(original) ?? 0;
usedNames.set(original, count + 1);
if (count === 0) return original;
const dot = original.lastIndexOf(".");
return dot === -1
? `${original}_${count}`
: `${original.slice(0, dot)}_${count}${original.slice(dot)}`;
}
const tasks = attachments.map(a => ({ attachment: a, filename: uniqueName(a.filename) }));
const results = await Promise.allSettled(tasks.map(async ({ attachment, filename }) => {
if (!attachment.proxy_url) throw new Error("Missing Proxy URL");
const res = await fetch(attachment.proxy_url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (!res.body) throw new Error("Response body is empty");
let fileHandle: FileSystemFileHandle | undefined;
try {
fileHandle = await dir.getFileHandle(filename, { create: true });
const writable = await fileHandle.createWritable();
try {
await res.body.pipeTo(writable);
} catch (e) {
await writable.abort();
throw e;
}
} catch (e) {
if (fileHandle) {
try { await dir.removeEntry(filename); } catch { }
}
throw e;
}
}));
const failed = results.filter(r => {
if (r.status === "rejected") {
logger.warn("Failed to download attachment:", r.reason);
return true;
}
return false;
}).length;
const succeeded = attachments.length - failed;
if (failed === 0)
showToast(`Downloaded ${pluralise(succeeded, "attachment")}.`, Toasts.Type.SUCCESS);
else
showToast(`Downloaded ${succeeded} of ${attachments.length} attachments. ${failed} failed.`, Toasts.Type.FAILURE);
}
export default definePlugin({
name: "DownloadAllAttachments",
description: "Adds a popover button to download all attachments in a message at once.",
tags: ["Utility", "Chat"],
authors: [EquicordDevs.dhopcs],
dependencies: ["MessagePopoverAPI"],
messagePopoverButton: {
icon: CloudDownloadIcon,
render(message: Message) {
if (!message.attachments.length) return null;
return {
label: "Download All Attachments",
icon: CloudDownloadIcon,
message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: () => downloadAll(message.attachments)
};
}
}
});

View file

@ -28,7 +28,7 @@ const logger = new Logger("FileUpload", "#7cb7ff");
function toProxyUrl(url: string): string { function toProxyUrl(url: string): string {
const corsProxyUrl = normalizeCorsProxyUrl((settings.store as { corsProxyUrl?: string; }).corsProxyUrl); const corsProxyUrl = normalizeCorsProxyUrl((settings.store as { corsProxyUrl?: string; }).corsProxyUrl);
if (Native || url.startsWith(`${corsProxyUrl}?url=`)) { if (url.startsWith(`${corsProxyUrl}?url=`)) {
return url; return url;
} }

View file

@ -8,10 +8,11 @@ import "./styles.css";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Paginator, requirePaginator } from "@plugins/reviewDB/components/ReviewModal"; import { Paginator, requirePaginator } from "@plugins/reviewDB/components/ReviewModal";
import { ModalCloseButton } from "@utils/modal";
import { Message } from "@vencord/discord-types"; import { Message } from "@vencord/discord-types";
import { findCssClassesLazy } from "@webpack"; import { findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
import { React, useRef, useState } from "@webpack/common"; import { React, useRef, useState } from "@webpack/common";
const CloseButton = findComponentByCodeLazy("CLOSE_BUTTON_LABEL");
import { MutableRefObject } from "react"; import { MutableRefObject } from "react";
import { jumper } from "./index"; import { jumper } from "./index";
@ -53,7 +54,7 @@ export default function ReplyNavigator({ replies }: { replies: Message[]; }) {
totalCount={replies.length} totalCount={replies.length}
onPageChange={processPageChange} onPageChange={processPageChange}
/> />
<ModalCloseButton className={"vc-findreply-close"} onClick={() => setVisible(false)} /> <CloseButton className={"vc-findreply-close"} onClick={() => setVisible(false)} />
</div> </div>
</ErrorBoundary> </ErrorBoundary>
); );

View file

@ -72,7 +72,7 @@ async function searchGoogleFonts(query: string) {
} }
const preloadFont = (family: string) => const preloadFont = (family: string) =>
loadFontStyle(createGoogleFontUrl(family, "&text=The quick brown fox jumps over the lazy dog")); loadFontStyle(createGoogleFontUrl(family, ":wght@400;700"));
let styleElement: HTMLStyleElement | null = null; let styleElement: HTMLStyleElement | null = null;
@ -136,14 +136,13 @@ function GoogleFontSearch({ onSelect }: { onSelect: (font: GoogleFontMetadata) =
return ( return (
<section> <section>
<HeadingSecondary>{t("equicord.fontLoader.ui.searchGoogleFonts")}</HeadingSecondary> <HeadingSecondary>{t("equicord.fontLoader.ui.searchGoogleFonts")}</HeadingSecondary>
<Paragraph>{t("equicord.fontLoader.ui.clickToApply")}</Paragraph> <Paragraph className={Margins.bottom8}>{t("equicord.fontLoader.ui.clickToApply")}</Paragraph>
<TextInput <TextInput
value={query} value={query}
onChange={e => handleSearch(e)} onChange={e => handleSearch(e)}
placeholder={t("equicord.fontLoader.ui.searchFonts")} placeholder={t("equicord.fontLoader.ui.searchFonts")}
disabled={loading} disabled={loading}
className={Margins.bottom16}
/> />
{results.length > 0 && ( {results.length > 0 && (

View file

@ -8,11 +8,11 @@ import "./styles.css";
import { BaseText } from "@components/BaseText"; import { BaseText } from "@components/BaseText";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { HeadingTertiary } from "@components/Heading"; import { Heading, HeadingTertiary } from "@components/Heading";
import { copyToClipboard } from "@utils/clipboard"; import { copyToClipboard } from "@utils/clipboard";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { findByPropsLazy, findCssClassesLazy } from "@webpack"; import { findByPropsLazy, findCssClassesLazy } from "@webpack";
import { Button, Forms, Parser, useEffect, useState } from "@webpack/common"; import { Button, Parser, useEffect, useState } from "@webpack/common";
import { FriendInvite } from "./types"; import { FriendInvite } from "./types";
@ -78,12 +78,12 @@ export default function FriendCodesPanel() {
return ( return (
<> <>
<header className={FormStyles.header}> <header className={FormStyles.header}>
<Forms.FormTitle <Heading
tag="h2" tag="h2"
className={FormStyles.title} className={FormStyles.title}
> >
Your Friend Codes Your Friend Codes
</Forms.FormTitle> </Heading>
<Flex <Flex
style={{ marginBottom: "16px" }} style={{ marginBottom: "16px" }}

View file

@ -13,10 +13,10 @@ import { Flex } from "@components/Flex";
import { Paragraph } from "@components/Paragraph"; import { Paragraph } from "@components/Paragraph";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Forms, RelationshipStore } from "@webpack/common"; import { RenderModalProps } from "@vencord/discord-types";
import { Forms, Modal,openModal, RelationshipStore } from "@webpack/common";
interface rankInfo { interface rankInfo {
title: string; title: string;
@ -85,10 +85,12 @@ const ranks: rankInfo[] =
]; ];
function openRankModal(rank: rankInfo) { function openRankModal(rank: rankInfo) {
openModal(props => ( openModal((props: RenderModalProps) => (
<ErrorBoundary> <ErrorBoundary>
<ModalRoot {...props} size={ModalSize.DYNAMIC}> <Modal
<ModalHeader> {...props}
size="sm"
title={
<Flex className={cl("flex")}> <Flex className={cl("flex")}>
<Forms.FormTitle <Forms.FormTitle
className={cl("img")} className={cl("img")}
@ -98,15 +100,14 @@ function openRankModal(rank: rankInfo) {
{rank.title} {rank.title}
</Forms.FormTitle> </Forms.FormTitle>
</Flex> </Flex>
</ModalHeader> }
<ModalContent> >
<div className={cl("text")}> <div className={cl("text")}>
<Paragraph> <Paragraph>
{rank.description} {rank.description}
</Paragraph> </Paragraph>
</div> </div>
</ModalContent> </Modal>
</ModalRoot>
</ErrorBoundary > </ErrorBoundary >
)); ));
} }

View file

@ -5,10 +5,9 @@
*/ */
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize } from "@utils/modal"; import { Channel, RenderModalProps } from "@vencord/discord-types";
import { Channel } from "@vencord/discord-types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Avatar, Button, ChannelStore, MessageStore, React, Text, UserStore } from "@webpack/common"; import { Avatar, Button, ChannelStore, MessageStore, Modal,React, Text, UserStore } from "@webpack/common";
const cl = classNameFactory("vc-boo-"); const cl = classNameFactory("vc-boo-");
@ -37,9 +36,8 @@ function GroupDmsIcon({ channel }: { channel: Channel; }) {
} }
interface GhostedUsersModalProps { interface GhostedUsersModalProps {
modalProps: any; modalProps: RenderModalProps;
ghostedChannels: string[]; ghostedChannels: string[];
onClose: () => void;
onClearGhost: (channelId: string) => void; onClearGhost: (channelId: string) => void;
} }
@ -57,14 +55,14 @@ export function getChannelDisplayName(channelId: string): string {
return user?.username || "Unknown User"; return user?.username || "Unknown User";
} }
export function GhostedUsersModal({ modalProps, ghostedChannels: initialChannels, onClose, onClearGhost }: GhostedUsersModalProps) { export function GhostedUsersModal({ modalProps, ghostedChannels: initialChannels, onClearGhost }: GhostedUsersModalProps) {
const [ghostedChannels, setGhostedChannels] = React.useState(initialChannels); const [ghostedChannels, setGhostedChannels] = React.useState(initialChannels);
const handleChannelClick = (channelId: string) => { const handleChannelClick = (channelId: string) => {
const channel = ChannelStore.getChannel(channelId); const channel = ChannelStore.getChannel(channelId);
if (channel) { if (channel) {
SelectedChannelActionCreators.selectPrivateChannel(channelId); SelectedChannelActionCreators.selectPrivateChannel(channelId);
onClose(); modalProps.onClose();
} }
}; };
@ -83,77 +81,67 @@ export function GhostedUsersModal({ modalProps, ghostedChannels: initialChannels
}; };
return ( return (
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}> <Modal
<ModalHeader> {...modalProps}
<Text size="md"
variant="heading-lg/semibold" title={`Ghosted Users (${ghostedChannels.length})`}
className={cl("modal-header")} actions={ghostedChannels.length > 0 ? [
> {
Ghosted Users ({ghostedChannels.length}) text: "Clear All",
</Text> variant: "primary",
{ghostedChannels.length > 0 && ( onClick: handleClearAll
<Button }
size={Button.Sizes.SMALL} ] : []}
color={Button.Colors.PRIMARY} >
onClick={handleClearAll} <div className={cl("modal-content")}>
className={cl("clear")} {ghostedChannels.length === 0 ? (
> <Text variant="text-md/normal">No ghosts here!</Text>
Clear All ) : (
</Button> ghostedChannels.map(channelId => {
)} const channel = ChannelStore.getChannel(channelId);
<ModalCloseButton onClick={onClose} /> if (!channel) return null;
</ModalHeader>
<ModalContent>
<div className={cl("modal-content")}>
{ghostedChannels.length === 0 ? (
<Text variant="text-md/normal">No ghosts here!</Text>
) : (
ghostedChannels.map(channelId => {
const channel = ChannelStore.getChannel(channelId);
if (!channel) return null;
const lastMessage = MessageStore.getMessages(channelId)?.last(); const lastMessage = MessageStore.getMessages(channelId)?.last();
const lastMessageDate = lastMessage?.timestamp ? formatMessageDate(lastMessage.timestamp) : ""; const lastMessageDate = lastMessage?.timestamp ? formatMessageDate(lastMessage.timestamp) : "";
const displayName = getChannelDisplayName(channel.id); const displayName = getChannelDisplayName(channel.id);
const userId = channel?.recipients?.[0] ?? ""; const userId = channel?.recipients?.[0] ?? "";
return ( return (
<div <div
key={channelId} key={channelId}
onClick={() => handleChannelClick(channelId)} onClick={() => handleChannelClick(channelId)}
className={cl("ghosted-entry")} className={cl("ghosted-entry")}
> >
{channel.isGroupDM() ? {channel.isGroupDM() ?
<GroupDmsIcon <GroupDmsIcon
channel={channel} channel={channel}
/> : <Avatar /> : <Avatar
src={UserStore.getUser(userId)?.getAvatarURL(undefined, 128, true)} src={UserStore.getUser(userId)?.getAvatarURL(undefined, 128, true)}
size="SIZE_40" size="SIZE_40"
aria-label={displayName} aria-label={displayName}
/>} />}
<div className={cl("user-info")}> <div className={cl("user-info")}>
<Text variant="text-md/normal"> <Text variant="text-md/normal">
{displayName} {displayName}
</Text>
{lastMessageDate && (
<Text variant="text-xs/normal" className={cl("modal-text")}>
{lastMessageDate}
</Text> </Text>
{lastMessageDate && ( )}
<Text variant="text-xs/normal" className={cl("modal-text")}>
{lastMessageDate}
</Text>
)}
</div>
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
onClick={e => handleClearClick(e, channelId)}
>
Clear
</Button>
</div> </div>
); <Button
}) size={Button.Sizes.SMALL}
)} color={Button.Colors.PRIMARY}
</div> onClick={e => handleClearClick(e, channelId)}
</ModalContent> >
</ModalRoot> Clear
</Button>
</div>
);
})
)}
</div>
</Modal>
); );
} }

View file

@ -12,11 +12,10 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs, EquicordDevs } from "@utils/constants"; import { Devs, EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { closeModal, openModal } from "@utils/modal";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Channel } from "@vencord/discord-types"; import { Channel } from "@vencord/discord-types";
import { Menu, Tooltip, useEffect, useState } from "@webpack/common"; import { Menu, openModal,Tooltip, useEffect, useState } from "@webpack/common";
import { Boo, clearChannelFromGhost, getBooCount, getGhostedChannels, onBooCountChange } from "./Boo"; import { Boo, clearChannelFromGhost, getBooCount, getGhostedChannels, onBooCountChange } from "./Boo";
import { getChannelDisplayName, GhostedUsersModal } from "./GhostedUsersModal"; import { getChannelDisplayName, GhostedUsersModal } from "./GhostedUsersModal";
@ -86,12 +85,11 @@ function BooIndicator() {
const handleClick = () => { const handleClick = () => {
const ghostedChannels = getGhostedChannels(); const ghostedChannels = getGhostedChannels();
const modalKey = openModal(modalProps => ( openModal(modalProps => (
<ErrorBoundary> <ErrorBoundary>
<GhostedUsersModal <GhostedUsersModal
modalProps={modalProps} modalProps={modalProps}
ghostedChannels={ghostedChannels} ghostedChannels={ghostedChannels}
onClose={() => closeModal(modalKey)}
onClearGhost={clearChannelFromGhost} onClearGhost={clearChannelFromGhost}
/> />
</ErrorBoundary> </ErrorBoundary>

View file

@ -8,8 +8,8 @@ import { Flex } from "@components/Flex";
import { Heading } from "@components/Heading"; import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph"; import { Paragraph } from "@components/Paragraph";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { RenderModalProps } from "@vencord/discord-types";
import { Button, FluxDispatcher, TextInput, useCallback, useState } from "@webpack/common"; import { Button, FluxDispatcher, Modal, openModal, TextInput, useCallback, useState } from "@webpack/common";
import { settings } from "../settings"; import { settings } from "../settings";
import { Collection, Gif } from "../types"; import { Collection, Gif } from "../types";
@ -49,124 +49,116 @@ export function openRenameCollectionModal(name: string) {
openModal(props => <RenameCollectionModal props={props} name={name} />); openModal(props => <RenameCollectionModal props={props} name={name} />);
} }
function InfoModal({ props, title, rows }: { props: ModalProps; title: string; rows: { label: string; value: string; }[]; }) { function InfoModal({ props, title, rows }: { props: RenderModalProps; title: string; rows: { label: string; value: string; }[]; }) {
return ( return (
<ModalRoot {...props} size={ModalSize.SMALL} className={cl("modal")}> <Modal
<ModalHeader separator={false} className={cl("modal-header")}> {...props}
<Paragraph className={cl("modal-title")}>{title}</Paragraph> size="sm"
</ModalHeader> title={title}
<ModalContent className={cl("modal-content")}> actions={[
<section> { text: "Close", variant: "secondary", onClick: props.onClose }
{rows.map(row => ( ]}
<Flex key={row.label} className={cl("info-row")}> >
<Heading className={cl("info-title")}>{row.label}</Heading> <section>
<Paragraph className={cl("info-text")}>{row.value}</Paragraph> {rows.map(row => (
</Flex> <Flex key={row.label} className={cl("info-row")}>
<Heading className={cl("info-title")}>{row.label}</Heading>
<Paragraph className={cl("info-text")}>{row.value}</Paragraph>
</Flex>
))}
</section>
</Modal>
);
}
function MoveToCollectionModal({ props, gifId }: { props: RenderModalProps; gifId: string; }) {
return (
<Modal
{...props}
size="sm"
title="Move To Collection"
actions={[
{ text: "Close", variant: "secondary", onClick: props.onClose }
]}
>
<Heading style={{ marginBottom: "10px" }}>Select a collection to move the item to</Heading>
<div className={cl("buttons")}>
{cache_collections
.filter(col => col.name !== getItemCollectionNameFromId(gifId))
.map(col => (
<Button
key={col.name}
className={cl("button")}
onClick={async () => {
const fromCollection = getItemCollectionNameFromId(gifId);
if (!fromCollection) return;
await moveGifToCollection(gifId, fromCollection, col.name);
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: "" });
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: fromCollection });
props.onClose();
}}
>
{stripPrefix(col.name)}
</Button>
))} ))}
</section> </div>
</ModalContent> </Modal>
<ModalFooter className={cl("modal-footer")}>
<Button onClick={props.onClose}>Close</Button>
</ModalFooter>
</ModalRoot>
); );
} }
function MoveToCollectionModal({ props, gifId }: { props: ModalProps; gifId: string; }) { function CreateCollectionModal({ props, gif }: { props: RenderModalProps; gif: Gif; }) {
return (
<ModalRoot {...props} size={ModalSize.SMALL} className={cl("modal")}>
<ModalHeader separator={false} className={cl("modal-header")}>
<Paragraph className={cl("modal-title")}>Move To Collection</Paragraph>
</ModalHeader>
<ModalContent className={cl("modal-content")}>
<Heading>Select a collection to move the item to</Heading>
<div className={cl("buttons")}>
{cache_collections
.filter(col => col.name !== getItemCollectionNameFromId(gifId))
.map(col => (
<Button
key={col.name}
className={cl("button")}
onClick={async () => {
const fromCollection = getItemCollectionNameFromId(gifId);
if (!fromCollection) return;
await moveGifToCollection(gifId, fromCollection, col.name);
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: "" });
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: fromCollection });
props.onClose();
}}
>
{stripPrefix(col.name)}
</Button>
))}
</div>
</ModalContent>
<ModalFooter className={cl("modal-footer")}>
<Button onClick={props.onClose}>Close</Button>
</ModalFooter>
</ModalRoot>
);
}
function CreateCollectionModal({ props, gif }: { props: ModalProps; gif: Gif; }) {
const [name, setName] = useState(""); const [name, setName] = useState("");
const onSubmit = useCallback((e: React.FormEvent) => { const onSubmit = useCallback((e?: React.FormEvent) => {
e.preventDefault(); e?.preventDefault();
if (!name.length) return; if (!name.length) return;
createCollection(name, [gif]); createCollection(name, [gif]);
props.onClose(); props.onClose();
}, [name, gif, props]); }, [name, gif, props]);
return ( return (
<ModalRoot {...props}> <Modal
{...props}
size="sm"
title="Create Collection"
actions={[
{ text: "Create", onClick: onSubmit, disabled: !name.length, variant: "primary" }
]}
>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<ModalHeader> <Heading className={cl("rename-text")}>Collection Name</Heading>
<Paragraph>Create Collection</Paragraph> <TextInput onChange={setName} />
</ModalHeader>
<ModalContent>
<Heading className={cl("rename-text")}>Collection Name</Heading>
<TextInput onChange={setName} />
</ModalContent>
<ModalFooter>
<Button type="submit" color={Button.Colors.GREEN} disabled={!name.length} onClick={onSubmit}>
Create
</Button>
</ModalFooter>
</form> </form>
</ModalRoot> </Modal>
); );
} }
function RenameCollectionModal({ props, name }: { props: ModalProps; name: string; }) { function RenameCollectionModal({ props, name }: { props: RenderModalProps; name: string; }) {
const prefix = settings.store.collectionPrefix; const prefix = settings.store.collectionPrefix;
const strippedName = name.startsWith(prefix) ? name.slice(prefix.length) : name; const strippedName = name.startsWith(prefix) ? name.slice(prefix.length) : name;
const [newName, setNewName] = useState(strippedName); const [newName, setNewName] = useState(strippedName);
const tooLong = newName.length >= 25; const tooLong = newName.length >= 25;
const onSubmit = useCallback(async (e: React.FormEvent) => { const onSubmit = useCallback(async (e?: React.FormEvent) => {
e.preventDefault(); e?.preventDefault();
if (!newName.length || tooLong) return; if (!newName.length || tooLong) return;
await renameCollection(name, newName); await renameCollection(name, newName);
props.onClose(); props.onClose();
}, [newName, name, tooLong, props]); }, [newName, name, tooLong, props]);
return ( return (
<ModalRoot {...props}> <Modal
{...props}
size="sm"
title="Rename Collection"
actions={[
{ text: "Rename", onClick: onSubmit, disabled: !newName.length || tooLong, variant: "primary" }
]}
>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<ModalHeader> <Paragraph className={cl("rename-text")}>New Collection Name</Paragraph>
<Paragraph>Rename Collection</Paragraph> <TextInput value={newName} className={classes(cl("rename-input"), tooLong ? cl("input-warning") : "")} onChange={setNewName} />
</ModalHeader> {tooLong && <Paragraph className={cl("warning-text")}>Name can't be longer than 24 characters</Paragraph>}
<ModalContent>
<Paragraph className={cl("rename-text")}>New Collection Name</Paragraph>
<TextInput value={newName} className={classes(cl("rename-input"), tooLong ? cl("input-warning") : "")} onChange={setNewName} />
{tooLong && <Paragraph className={cl("warning-text")}>Name can't be longer than 24 characters</Paragraph>}
</ModalContent>
<ModalFooter>
<Button type="submit" color={Button.Colors.GREEN} disabled={!newName.length || tooLong} onClick={onSubmit}>
Rename
</Button>
</ModalFooter>
</form> </form>
</ModalRoot> </Modal>
); );
} }

View file

@ -9,10 +9,9 @@ import { Span } from "@components/Span";
import { fetchReposByUserId, fetchReposByUsername, fetchUserInfo, GitHubUserInfo } from "@equicordplugins/githubRepos/githubApi"; import { fetchReposByUserId, fetchReposByUsername, fetchUserInfo, GitHubUserInfo } from "@equicordplugins/githubRepos/githubApi";
import { GitHubRepo } from "@equicordplugins/githubRepos/types"; import { GitHubRepo } from "@equicordplugins/githubRepos/types";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { openModal } from "@utils/modal";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { findCssClassesLazy } from "@webpack"; import { findCssClassesLazy } from "@webpack";
import { Clickable, React, useEffect, UserProfileStore, useState } from "@webpack/common"; import { Clickable, openModal,React, useEffect, UserProfileStore, useState } from "@webpack/common";
import { ReposModal } from "./ReposModal"; import { ReposModal } from "./ReposModal";

View file

@ -7,9 +7,8 @@
import { BaseText } from "@components/BaseText"; import { BaseText } from "@components/BaseText";
import { fetchReposByUserId, fetchReposByUsername, fetchUserInfo, GitHubUserInfo } from "@equicordplugins/githubRepos/githubApi"; import { fetchReposByUserId, fetchReposByUsername, fetchUserInfo, GitHubUserInfo } from "@equicordplugins/githubRepos/githubApi";
import { GitHubRepo } from "@equicordplugins/githubRepos/types"; import { GitHubRepo } from "@equicordplugins/githubRepos/types";
import { openModal } from "@utils/modal";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { React, useEffect, UserProfileStore, useState } from "@webpack/common"; import { openModal, React, useEffect, UserProfileStore, useState } from "@webpack/common";
import { cl, settings } from ".."; import { cl, settings } from "..";
import { RepoCard } from "./RepoCard"; import { RepoCard } from "./RepoCard";

View file

@ -4,12 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { Button } from "@components/Button";
import { HeadingPrimary } from "@components/Heading";
import { getLanguageColor } from "@equicordplugins/githubRepos/colors"; import { getLanguageColor } from "@equicordplugins/githubRepos/colors";
import { GitHubRepo } from "@equicordplugins/githubRepos/types"; import { GitHubRepo } from "@equicordplugins/githubRepos/types";
import { ModalContent, ModalFooter, ModalHeader, ModalRoot } from "@utils/modal"; import { RenderModalProps } from "@vencord/discord-types";
import { React } from "@webpack/common"; import { Modal, React } from "@webpack/common";
import { cl } from ".."; import { cl } from "..";
import { Star } from "./Star"; import { Star } from "./Star";
@ -20,7 +18,7 @@ interface ReposModalProps {
rootProps: any; rootProps: any;
} }
export function ReposModal({ repos, username, rootProps }: ReposModalProps) { export function ReposModal({ repos, username, rootProps }: ReposModalProps & { rootProps: RenderModalProps; }) {
const renderTableHeader = () => ( const renderTableHeader = () => (
<thead> <thead>
<tr> <tr>
@ -63,44 +61,38 @@ export function ReposModal({ repos, username, rootProps }: ReposModalProps) {
); );
return ( return (
<ModalRoot className={cl("modal")} size="large" {...rootProps}> <Modal
<ModalHeader> {...rootProps}
<HeadingPrimary className={cl("modal-title")}> className={cl("modal")}
{username}'s GitHub Repositories size="lg"
</HeadingPrimary> title={`${username}'s GitHub Repositories`}
</ModalHeader> actions={[
<ModalContent className={cl("modal-content")}> {
<div className={cl("table-container")}> text: "View on GitHub",
<table className={cl("table")}> variant: "link",
<colgroup> onClick: () => window.open(`https://github.com/${username}?tab=repositories`, "_blank")
<col className={cl("header-repo")} /> },
<col className={cl("header-description")} /> {
<col className={cl("header-language")} /> text: "Close",
<col className={cl("header-stars")} /> variant: "secondary",
</colgroup> onClick: rootProps.onClose
{renderTableHeader()} }
<tbody> ]}
{repos.map(renderTableRow)} >
</tbody> <div className={cl("table-container")}>
</table> <table className={cl("table")}>
</div> <colgroup>
</ModalContent> <col className={cl("header-repo")} />
<ModalFooter className={cl("modal-footer")}> <col className={cl("header-description")} />
<Button <col className={cl("header-language")} />
className={cl("modal-footer-github")} <col className={cl("header-stars")} />
variant="link" </colgroup>
onClick={() => window.open(`https://github.com/${username}?tab=repositories`, "_blank")} {renderTableHeader()}
> <tbody>
View on GitHub {repos.map(renderTableRow)}
</Button> </tbody>
<Button </table>
className={cl("modal-footer-close")} </div>
variant="secondary" </Modal>
onClick={rootProps.onClose}
>
Close
</Button>
</ModalFooter>
</ModalRoot>
); );
} }

View file

@ -65,13 +65,16 @@ export default definePlugin({
shouldShowGitHub(userId: string) { shouldShowGitHub(userId: string) {
return UserProfileStore.getUserProfile(userId)?.connectedAccounts?.some((c: any) => c.type === "github") ?? false; return UserProfileStore.getUserProfile(userId)?.connectedAccounts?.some((c: any) => c.type === "github") ?? false;
}, },
renderProfileCollection: (props: { user: User; displayProfile?: any; }) => { renderProfileCollection: {
return ( render: (props: { user: User; displayProfile?: any; }) => {
<ProfilePopoutComponent return (
{...props} <ProfilePopoutComponent
id={props.user.id} {...props}
/> id={props.user.id}
); />
);
},
priority: 0,
}, },
renderProfileRepositoriesTab: ErrorBoundary.wrap((props: { user: User; displayProfile?: any; }) => { renderProfileRepositoriesTab: ErrorBoundary.wrap((props: { user: User; displayProfile?: any; }) => {
return ( return (

View file

@ -4,14 +4,12 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { Button } from "@components/Button";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { HeadingTertiary } from "@components/Heading"; import { HeadingTertiary } from "@components/Heading";
import { Paragraph } from "@components/Paragraph"; import { Paragraph } from "@components/Paragraph";
import { openInviteModal } from "@utils/discord"; import { openInviteModal } from "@utils/discord";
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal"; import { RenderModalProps, User } from "@vencord/discord-types";
import { User } from "@vencord/discord-types"; import { Modal, openModal, React, Tooltip } from "@webpack/common";
import { React, Tooltip } from "@webpack/common";
import { cl, GlobalBadges, INVITE_LINK } from "./utils"; import { cl, GlobalBadges, INVITE_LINK } from "./utils";
@ -37,20 +35,32 @@ export function openBadgeModal(user: User) {
<BadgeModalComponent key={badge.key} tooltip={badge.tooltip} badge={badge.badge} /> <BadgeModalComponent key={badge.key} tooltip={badge.tooltip} badge={badge.badge} />
)); ));
openModal(modalprops => ( openModal((modalprops: RenderModalProps) => (
<ModalRoot {...modalprops}> <Modal
<ErrorBoundary> {...modalprops}
<ModalHeader> size="sm"
title={
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<img <img
className={cl("modal-avatar")} className={cl("modal-avatar")}
src={user.getAvatarURL(undefined, 512, true)} src={user.getAvatarURL(undefined, 512, true)}
alt="" alt=""
/> />
<HeadingTertiary className={cl("modal-name")}> <HeadingTertiary className={cl("modal-name")} style={{ color: "white" }}>
{user.username} {user.username}
</HeadingTertiary> </HeadingTertiary>
</ModalHeader> </div>
<ModalContent className={cl("modal-description")}> }
actions={[
{
text: "Join GlobalBadges Server",
variant: "link",
onClick: () => openInviteModal(INVITE_LINK)
}
]}
>
<ErrorBoundary>
<div className={cl("modal-description")}>
{badgeDataElements.length ? ( {badgeDataElements.length ? (
<> <>
<Paragraph className={cl("modal-text")}> <Paragraph className={cl("modal-text")}>
@ -66,16 +76,8 @@ export function openBadgeModal(user: User) {
No global badges. No global badges.
</Paragraph> </Paragraph>
)} )}
</ModalContent> </div>
<ModalFooter>
<Button
variant="link"
onClick={() => openInviteModal(INVITE_LINK)}
>
Join GlobalBadges Server
</Button>
</ModalFooter>
</ErrorBoundary> </ErrorBoundary>
</ModalRoot> </Modal>
)); ));
} }

View file

@ -89,8 +89,11 @@ export default definePlugin({
dependencies: ["ChatInputButtonAPI"], dependencies: ["ChatInputButtonAPI"],
settings: settings, settings: settings,
startAt: StartAt.Init, startAt: StartAt.Init,
chatBarButtonWrapper: (buttons: ReactNode) => { chatBarButtonWrapper: {
return <ButtonsInnerComponent buttons={buttons} />; wrapper: (buttons: ReactNode) => {
return <ButtonsInnerComponent buttons={buttons} />;
},
priority: 0,
}, },
start() { start() {
hidechatbuttonsopen = settings.store.open; hidechatbuttonsopen = settings.store.open;

View file

@ -5,13 +5,33 @@
*/ */
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { isPluginEnabled } from "@api/PluginManager";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { EyeIcon } from "@components/Icons"; import { EyeIcon } from "@components/Icons";
import pinDms from "@plugins/pinDms";
import { isPinned } from "@plugins/pinDms/data";
import { EquicordDevs } from "@utils/constants"; import { EquicordDevs } from "@utils/constants";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Message } from "@vencord/discord-types"; import { Channel, Message } from "@vencord/discord-types";
import { ChannelStore, FluxDispatcher, Menu } from "@webpack/common"; import { ChannelStore, Clickable, FluxDispatcher, Menu, Tooltip } from "@webpack/common";
interface UserContextProps {
channel?: Channel;
}
interface PrivateChannelsListInstance {
forceUpdate(callback?: () => void): void;
}
const hiddenDmIds = new Set<string>();
let privateChannelsListInstance: PrivateChannelsListInstance | null = null;
let showHiddenDms = false;
function notifyHiddenDmsUpdate() {
privateChannelsListInstance?.forceUpdate();
}
const hideMessage = (messageId: string, channelId: string) => { const hideMessage = (messageId: string, channelId: string) => {
FluxDispatcher.dispatch({ FluxDispatcher.dispatch({
@ -22,6 +42,17 @@ const hideMessage = (messageId: string, channelId: string) => {
}); });
}; };
function toggleDm(channelId: string) {
if (hiddenDmIds.has(channelId)) {
hiddenDmIds.delete(channelId);
if (!hiddenDmIds.size) showHiddenDms = false;
} else {
hiddenDmIds.add(channelId);
}
notifyHiddenDmsUpdate();
}
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => { const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => {
const group = findGroupChildrenByChildId("copy-text", children); const group = findGroupChildrenByChildId("copy-text", children);
if (!group) return; if (!group) return;
@ -36,6 +67,25 @@ const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }: { m
)); ));
}; };
const userCtxPatch: NavContextMenuPatchCallback = (children, { channel }: UserContextProps) => {
if (!channel?.isDM()) return;
if (isPluginEnabled(pinDms.name) && isPinned(channel.id)) return;
const group = findGroupChildrenByChildId("close-dm", children);
if (!group) return;
const hidden = hiddenDmIds.has(channel.id);
group.splice(group.findIndex(c => c?.props?.id === "close-dm"), 0, (
<Menu.MenuItem
id="vc-hidemessages-dm"
label={hidden ? "Unhide DM" : "Hide DM"}
icon={EyeIcon}
action={() => toggleDm(channel.id)}
/>
));
};
const settings = definePluginSettings({ const settings = definePluginSettings({
hidePopoverButton: { hidePopoverButton: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
@ -50,10 +100,71 @@ export default definePlugin({
dependencies: ["MessagePopoverAPI"], dependencies: ["MessagePopoverAPI"],
tags: ["Chat", "Utility"], tags: ["Chat", "Utility"],
authors: [EquicordDevs.yash], authors: [EquicordDevs.yash],
patches: [
{
find: '"dm-quick-launcher"===',
replacement: [
{
match: /render\(\)\{/,
replace: "$&this.props.privateChannelIds=$self.filterPrivateChannelIds(this.props.privateChannelIds,this);"
},
{
match: /renderRow=\i=>\{/,
replace: "$&this.props.privateChannelIds=$self.filterPrivateChannelIds(this.props.privateChannelIds,this);"
},
{
match: /renderDM=\(\i,\i\)=>\{/,
replace: "$&this.props.privateChannelIds=$self.filterPrivateChannelIds(this.props.privateChannelIds,this);"
},
{
match: /#{intl::DIRECT_MESSAGES}\)\}\),/,
replace: "$&$self.renderHiddenMessagesToggle(),"
}
]
}
],
contextMenus: { contextMenus: {
"message": messageCtxPatch "message": messageCtxPatch,
"user-context": userCtxPatch
}, },
settings, settings,
stop() {
hiddenDmIds.clear();
showHiddenDms = false;
notifyHiddenDmsUpdate();
privateChannelsListInstance = null;
},
filterPrivateChannelIds(privateChannelIds: string[], instance?: PrivateChannelsListInstance) {
privateChannelsListInstance = instance ?? privateChannelsListInstance;
return showHiddenDms ? privateChannelIds : privateChannelIds.filter(id => !hiddenDmIds.has(id));
},
renderHiddenMessagesToggle: ErrorBoundary.wrap(() => {
const hasHiddenDms = hiddenDmIds.size > 0;
const label = !hasHiddenDms ? "No Hidden DMs" : showHiddenDms ? "Hide Hidden DMs" : "Show Hidden DMs";
return (
<Tooltip text={label}>
{tooltipProps => (
<Clickable
{...tooltipProps}
role="button"
tabIndex={0}
aria-label={label}
aria-disabled={!hasHiddenDms}
onClick={event => {
event.preventDefault();
event.stopPropagation();
if (!hasHiddenDms) return;
showHiddenDms = !showHiddenDms;
notifyHiddenDmsUpdate();
}}
>
<EyeIcon width={18} height={18} />
</Clickable>
)}
</Tooltip>
);
}, { noop: true }),
messagePopoverButton: { messagePopoverButton: {
icon: EyeIcon, icon: EyeIcon,
render(message: Message) { render(message: Message) {

View file

@ -10,40 +10,21 @@ import { HiddenServersStore } from "@equicordplugins/hideServers/HiddenServersSt
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { getGuildAcronym } from "@utils/discord"; import { getGuildAcronym } from "@utils/discord";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { import { Guild, RenderModalProps } from "@vencord/discord-types";
closeModal,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalProps,
ModalRoot,
ModalSize,
openModal,
} from "@utils/modal";
import { Guild } from "@vencord/discord-types";
import { findCssClassesLazy } from "@webpack"; import { findCssClassesLazy } from "@webpack";
import { Button, IconUtils, useStateFromStores } from "@webpack/common"; import { Button, closeModal, IconUtils, Modal,openModal, useStateFromStores } from "@webpack/common";
import { SortedGuildStore } from ".."; import { SortedGuildStore } from "..";
const cl = classNameFactory("vc-hideservers-"); const cl = classNameFactory("vc-hideservers-");
const IconClasses = findCssClassesLazy("icon", "acronym", "childWrapper"); const IconClasses = findCssClassesLazy("icon", "acronym", "childWrapper");
function HiddenServersModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) { function HiddenServersModal({ modalProps, close }: { modalProps: RenderModalProps; close(): void; }) {
const guilds = useStateFromStores([HiddenServersStore], () => HiddenServersStore.hiddenGuildsDetail()); const guilds = useStateFromStores([HiddenServersStore], () => HiddenServersStore.hiddenGuildsDetail());
return ( return (
<ModalRoot {...modalProps} size={ModalSize.LARGE}> <Modal {...modalProps} size="lg" title="Hidden Servers">
<ModalHeader> <HiddenServersMenu guilds={guilds} />
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}> </Modal>
Hidden Servers
</BaseText>
<ModalCloseButton onClick={close} />
</ModalHeader>
<ModalContent>
<HiddenServersMenu guilds={guilds} />
</ModalContent>
</ModalRoot>
); );
} }

View file

@ -5,7 +5,6 @@
*/ */
import { BaseText } from "@components/BaseText"; import { BaseText } from "@components/BaseText";
import { Button } from "@components/Button";
import { CodeBlock } from "@components/CodeBlock"; import { CodeBlock } from "@components/CodeBlock";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { HeadingSecondary } from "@components/Heading"; import { HeadingSecondary } from "@components/Heading";
@ -13,18 +12,8 @@ import { Paragraph } from "@components/Paragraph";
import { TooltipContainer } from "@components/TooltipContainer"; import { TooltipContainer } from "@components/TooltipContainer";
import { copyWithToast, getIntlMessage } from "@utils/discord"; import { copyWithToast, getIntlMessage } from "@utils/discord";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import {
CloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
ModalSize,
openModal
} from "@utils/modal";
import { saveFile } from "@utils/web"; import { saveFile } from "@utils/web";
import { Icon } from "@vencord/discord-types"; import { Icon, RenderModalProps } from "@vencord/discord-types";
import { findComponentByCodeLazy } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import { import {
Clickable, Clickable,
@ -32,6 +21,8 @@ import {
createRoot, createRoot,
FluxDispatcher, FluxDispatcher,
Menu, Menu,
Modal,
openModal,
ReactDOM, ReactDOM,
useCallback, useCallback,
useEffect, useEffect,
@ -226,7 +217,7 @@ function OtherContextMenu({ iconName, Icon, color }: { iconName: string; Icon: I
); );
} }
function IconModal({ iconName, Icon, onClose, transitionState }: { iconName: string; Icon: Icon; } & ModalProps) { function IconModal({ iconName, Icon, onClose, transitionState }: { iconName: string; Icon: Icon; } & RenderModalProps) {
const [color, setColor] = useColorNavigation(209); const [color, setColor] = useColorNavigation(209);
const colorData = cssColors[color]; const colorData = cssColors[color];
const colorKeys = useMemo(() => getCssColorKeys(), []); const colorKeys = useMemo(() => getCssColorKeys(), []);
@ -247,62 +238,59 @@ function IconModal({ iconName, Icon, onClose, transitionState }: { iconName: str
}); });
}, [colorKeys.length, setColor]); }, [colorKeys.length, setColor]);
const openOtherMenu = (e: React.MouseEvent) => { const openOtherMenu = (e?: React.MouseEvent) => {
ContextMenuApi.openContextMenu(e, () => ( if (e) ContextMenuApi.openContextMenu(e, () => (
<OtherContextMenu iconName={iconName} Icon={Icon} color={color} /> <OtherContextMenu iconName={iconName} Icon={Icon} color={color} />
)); ));
}; };
return ( return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}> <Modal
<ModalHeader separator={false} className="vc-ic-modal-header"> transitionState={transitionState}
<div className="vc-ic-modal-header-content"> onClose={onClose}
<BaseText size="lg" weight="semibold" className="vc-ic-modal-title">{iconName}</BaseText> size="md"
title={iconName}
actions={[
{
text: "Actions",
variant: "primary",
onClick: openOtherMenu
}
]}
>
<Flex className="vc-ic-modal-main">
<div
className="vc-ic-icon-preview"
aria-label={colorData?.name}
onContextMenu={openColorMenu}
onWheel={onWheel}
>
<Icon className="vc-ic-icon-large" color={colorData?.css} fill={fill} />
</div> </div>
<div className="vc-ic-modal-header-trailing"> <Flex flexDirection="column" className="vc-ic-icon-info">
<CloseButton onClick={onClose} /> <Flex className="vc-ic-icon-sizes">
</div> {iconSizes.map(size => (
</ModalHeader> <TooltipContainer text={size} key={size}>
<ModalContent className="vc-ic-modal-content"> <Icon size={size} color={colorData?.css} fill={fill} />
<Flex className="vc-ic-modal-main"> </TooltipContainer>
<div ))}
className="vc-ic-icon-preview"
aria-label={colorData?.name}
onContextMenu={openColorMenu}
onWheel={onWheel}
>
<Icon className="vc-ic-icon-large" color={colorData?.css} fill={fill} />
</div>
<Flex flexDirection="column" className="vc-ic-icon-info">
<Flex className="vc-ic-icon-sizes">
{iconSizes.map(size => (
<TooltipContainer text={size} key={size}>
<Icon size={size} color={colorData?.css} fill={fill} />
</TooltipContainer>
))}
</Flex>
<TooltipContainer text="Right-click icon to change">
<BaseText size="sm" color="text-muted" className="vc-ic-color-label">
{colorData?.name}
</BaseText>
</TooltipContainer>
</Flex> </Flex>
<TooltipContainer text="Right-click icon to change">
<BaseText size="sm" color="text-muted" className="vc-ic-color-label">
{colorData?.name}
</BaseText>
</TooltipContainer>
</Flex> </Flex>
<div className="vc-ic-use-as"> </Flex>
<BaseText size="md" weight="semibold">Usage</BaseText> <div className="vc-ic-use-as">
<BaseText size="sm" color="text-muted">Click to copy</BaseText> <BaseText size="md" weight="semibold">Usage</BaseText>
</div> <BaseText size="sm" color="text-muted">Click to copy</BaseText>
{/* for some reason i cant make this shit codeblock full width, FF 15 */} </div>
<Clickable className="vc-ic-codeblock-wrapper" onClick={() => copyWithToast(findCode, "Copied!")}> {/* for some reason i cant make this shit codeblock full width, FF 15 */}
<CodeBlock content={findCode} lang="ts" /> <Clickable className="vc-ic-codeblock-wrapper" onClick={() => copyWithToast(findCode, "Copied!")}>
</Clickable> <CodeBlock content={findCode} lang="ts" />
</ModalContent> </Clickable>
<ModalFooter className="vc-ic-modal-footer"> </Modal>
<Button variant="primary" onClick={openOtherMenu}>
Actions
</Button>
</ModalFooter>
</ModalRoot>
); );
} }

View file

@ -22,7 +22,7 @@ export default definePlugin({
description: t("equicord.iconViewer.description"), description: t("equicord.iconViewer.description"),
tags: ["Developers"], tags: ["Developers"],
authors: [EquicordDevs.iamme], authors: [EquicordDevs.iamme],
dependencies: ["Settings"], dependencies: ["Settings", "ConcatenatedModules"],
startAt: StartAt.WebpackReady, startAt: StartAt.WebpackReady,
toolboxActions: { toolboxActions: {
[t("equicord.iconViewer.ui.openIconsTab")]() { [t("equicord.iconViewer.ui.openIconsTab")]() {

View file

@ -39,8 +39,7 @@ const filterOngoingRings = (currentUserId: string): CallUpdate["ongoingRings"] =
args.ongoingRings.filter((id: string) => id !== currentUserId); args.ongoingRings.filter((id: string) => id !== currentUserId);
const ContextMenuPatch: NavContextMenuPatchCallback = (children, { channel }: { channel: Channel; }) => { const ContextMenuPatch: NavContextMenuPatchCallback = (children, { channel }: { channel: Channel; }) => {
if (!channel || (!channel.isDM() && !channel.isGroupDM())) return; if (!channel) return;
const permanentlyIgnoredUsers = settings.store.permanentlyIgnoredUsers.split(",").map(s => s.trim()).filter(Boolean); const permanentlyIgnoredUsers = settings.store.permanentlyIgnoredUsers.split(",").map(s => s.trim()).filter(Boolean);
const [tempChecked, setTempChecked] = React.useState(ignoredChannelIds.has(channel.id)); const [tempChecked, setTempChecked] = React.useState(ignoredChannelIds.has(channel.id));

View file

@ -7,38 +7,35 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Paragraph } from "@components/Paragraph"; import { Paragraph } from "@components/Paragraph";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { GuildMember, RenderModalProps } from "@vencord/discord-types";
import { GuildMember } from "@vencord/discord-types"; import { Modal,openModal, Parser, React } from "@webpack/common";
import { Parser } from "@webpack/common";
const cl = classNameFactory("vc-inrole-"); const cl = classNameFactory("vc-inrole-");
export function showInRoleModal(members: GuildMember[], roleId: string, channelId: string) { export function showInRoleModal(members: GuildMember[], roleId: string, channelId: string) {
openModal(props => openModal((props: RenderModalProps) =>
<> <ErrorBoundary>
<ErrorBoundary> <Modal
<ModalRoot {...props} size={ModalSize.DYNAMIC} fullscreenOnMobile={true} > {...props}
<ModalHeader className={cl("header")}> size="md"
<Paragraph style={{ fontSize: "1.2rem", fontWeight: "bold", marginRight: "7px" }}>Members of role { title={
Parser.parse(`<@&${roleId}>`, true, { channelId, viewingChannelId: channelId }) <Paragraph style={{ fontSize: "1.2rem", fontWeight: "bold", marginRight: "7px" }}>
} ({members.length})</Paragraph> Members of role {Parser.parse(`<@&${roleId}>`, true, { channelId, viewingChannelId: channelId })} ({members.length})
<ModalCloseButton onClick={props.onClose} className={cl("close")} /> </Paragraph>
</ModalHeader> }
<ModalContent> >
<div style={{ padding: "13px 20px" }} className={cl("member-list")}> <div style={{ padding: "13px 20px" }} className={cl("member-list")}>
{ {
members.length !== 0 ? members.map(member => members.length !== 0 ? members.map(member =>
<> <React.Fragment key={member.userId}>
<Paragraph className={cl("modal-member")}> <Paragraph className={cl("modal-member")}>
{Parser.parse(`<@${member.userId}>`, true, { channelId, viewingChannelId: channelId })} {Parser.parse(`<@${member.userId}>`, true, { channelId, viewingChannelId: channelId })}
</Paragraph> </Paragraph>
</> </React.Fragment>
) : <Paragraph>Looks like no online cached members with that role were found. Try scrolling down on your member list to cache more users!</Paragraph> ) : <Paragraph>Looks like no online cached members with that role were found. Try scrolling down on your member list to cache more users!</Paragraph>
} }
</div> </div>
</ModalContent> </Modal>
</ModalRoot> </ErrorBoundary>
</ErrorBoundary>
</>
); );
} }

View file

@ -16,60 +16,50 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Heading, HeadingTertiary } from "@components/Heading"; import { Heading } from "@components/Heading";
import {
ModalContent,
ModalFooter,
ModalHeader,
ModalRoot,
openModal,
} from "@utils/modal";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { Button, React, TextInput } from "@webpack/common"; import { RenderModalProps } from "@vencord/discord-types";
import { Modal, openModal, React, TextInput } from "@webpack/common";
import { buildEmbed, decrypt } from "../index"; import { buildEmbed, decrypt } from "../index";
export function DecModal(props: any) { export function DecModal(props: RenderModalProps & { message: any; }) {
const encryptedMessage: string = props?.message?.content; const encryptedMessage: string = props?.message?.content;
const [password, setPassword] = React.useState("password"); const [password, setPassword] = React.useState("password");
const onDecrypt = () => {
const toSend = decrypt(encryptedMessage, password, true);
if (!toSend || !props?.message) return;
buildEmbed(props?.message, toSend);
props.onClose();
};
return ( return (
<ModalRoot {...props}> <Modal
<ModalHeader> {...props}
<HeadingTertiary>Decrypt Message</HeadingTertiary> size="sm"
</ModalHeader> title="Decrypt Message"
actions={[
<ModalContent> {
<Heading style={{ marginTop: "10px" }}>Message with Encryption</Heading> text: "Decrypt",
<TextInput defaultValue={encryptedMessage} disabled={true}></TextInput> variant: "primary",
<Heading style={{ marginTop: "10px" }}>Password</Heading> onClick: onDecrypt
<TextInput },
style={{ marginBottom: "20px" }} {
onChange={setPassword} text: t("vencord.cancel"),
/> variant: "secondary",
</ModalContent> onClick: props.onClose
}
<ModalFooter> ]}
<Button >
color={Button.Colors.GREEN} <Heading style={{ marginTop: "10px" }}>Message with Encryption</Heading>
onClick={() => { <TextInput defaultValue={encryptedMessage} disabled={true}></TextInput>
const toSend = decrypt(encryptedMessage, password, true); <Heading style={{ marginTop: "10px" }}>Password</Heading>
if (!toSend || !props?.message) return; <TextInput
buildEmbed(props?.message, toSend); style={{ marginBottom: "20px" }}
props.onClose(); onChange={setPassword}
}}> />
Decrypt </Modal>
</Button>
<Button
color={Button.Colors.TRANSPARENT}
look={Button.Looks.LINK}
style={{ left: 15, position: "absolute" }}
onClick={props.onClose}
>
{t("vencord.cancel")}
</Button>
</ModalFooter>
</ModalRoot>
); );
} }

View file

@ -17,22 +17,15 @@
*/ */
import { FormSwitch } from "@components/FormSwitch"; import { FormSwitch } from "@components/FormSwitch";
import { Heading, HeadingTertiary } from "@components/Heading"; import { Heading } from "@components/Heading";
import { insertTextIntoChatInputBox } from "@utils/discord"; import { insertTextIntoChatInputBox } from "@utils/discord";
import {
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
openModal,
} from "@utils/modal";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { Button, React, TextInput } from "@webpack/common"; import { RenderModalProps } from "@vencord/discord-types";
import { Modal, openModal, React, TextInput } from "@webpack/common";
import { encrypt } from "../index"; import { encrypt } from "../index";
function EncModal(props: ModalProps) { function EncModal(props: RenderModalProps) {
const [secret, setSecret] = React.useState(""); const [secret, setSecret] = React.useState("");
const [cover, setCover] = React.useState(""); const [cover, setCover] = React.useState("");
const [password, setPassword] = React.useState("password"); const [password, setPassword] = React.useState("password");
@ -40,72 +33,65 @@ function EncModal(props: ModalProps) {
const isValid = secret && (noCover || (cover && cover.trim().split(" ").length > 1)); const isValid = secret && (noCover || (cover && cover.trim().split(" ").length > 1));
const onSend = () => {
if (!isValid) return;
const encrypted = encrypt(secret, password, noCover ? "d d" : cover);
const toSend = noCover ? encrypted.replaceAll("d", "") : encrypted;
if (!toSend) return;
insertTextIntoChatInputBox(toSend);
props.onClose();
};
return ( return (
<ModalRoot {...props}> <Modal
<ModalHeader> {...props}
<HeadingTertiary>Encrypt Message</HeadingTertiary> size="sm"
</ModalHeader> title="Encrypt Message"
actions={[
<ModalContent> {
<Heading style={{ marginTop: "10px" }}>Secret</Heading> text: "Send",
<TextInput variant: "primary",
onChange={(e: string) => { disabled: !isValid,
setSecret(e); onClick: onSend
}} },
/> {
<Heading style={{ marginTop: "10px" }}>Cover (2 or more Words!!)</Heading> text: t("vencord.cancel"),
<TextInput variant: "secondary",
disabled={noCover} onClick: props.onClose
onChange={(e: string) => { }
setCover(e); ]}
}} >
/> <Heading style={{ marginTop: "10px" }}>Secret</Heading>
<Heading style={{ marginTop: "10px" }}>Password</Heading> <TextInput
<TextInput onChange={(e: string) => {
style={{ marginBottom: "20px" }} setSecret(e);
defaultValue={"password"} }}
onChange={(e: string) => { />
setPassword(e); <Heading style={{ marginTop: "10px" }}>Cover (2 or more Words!!)</Heading>
}} <TextInput
/> disabled={noCover}
<FormSwitch onChange={(e: string) => {
title="Don't use a Cover" setCover(e);
value={noCover} }}
onChange={(e: boolean) => { />
setNoCover(e); <Heading style={{ marginTop: "10px" }}>Password</Heading>
}} <TextInput
/> style={{ marginBottom: "20px" }}
</ModalContent> defaultValue={"password"}
onChange={(e: string) => {
<ModalFooter> setPassword(e);
<Button }}
color={Button.Colors.GREEN} />
disabled={!isValid} <FormSwitch
onClick={() => { title="Don't use a Cover"
if (!isValid) return; value={noCover}
const encrypted = encrypt(secret, password, noCover ? "d d" : cover); onChange={(e: boolean) => {
const toSend = noCover ? encrypted.replaceAll("d", "") : encrypted; setNoCover(e);
if (!toSend) return; }}
/>
insertTextIntoChatInputBox(toSend); </Modal>
props.onClose();
}}
>
Send
</Button>
<Button
color={Button.Colors.TRANSPARENT}
look={Button.Looks.LINK}
style={{ left: 15, position: "absolute" }}
onClick={() => {
props.onClose();
}}
>
{t("vencord.cancel")}
</Button>
</ModalFooter>
</ModalRoot>
); );
} }

View file

@ -9,14 +9,14 @@ import "./style.css";
import { actions } from "@equicordplugins/keyboardNavigation/commands"; import { actions } from "@equicordplugins/keyboardNavigation/commands";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { closeAllModals, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { RenderModalProps } from "@vencord/discord-types";
import { React, TextInput, useEffect, useState } from "@webpack/common"; import { closeAllModals, Modal,openModal, React, TextInput, useEffect, useState } from "@webpack/common";
import { settings } from ".."; import { settings } from "..";
const logger = new Logger("CommandPalette", "#e5c890"); const logger = new Logger("CommandPalette", "#e5c890");
export function CommandPalette({ modalProps }) { export function CommandPalette({ modalProps }: { modalProps: RenderModalProps; }) {
const cl = classNameFactory("vc-command-palette-"); const cl = classNameFactory("vc-command-palette-");
const [focusedIndex, setFocusedIndex] = useState<number | null>(null); const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
const [startIndex, setStartIndex] = useState(0); const [startIndex, setStartIndex] = useState(0);
@ -100,8 +100,8 @@ export function CommandPalette({ modalProps }) {
}, [queryEh]); }, [queryEh]);
return ( return (
<ModalRoot className={cl("root")} {...modalProps} size={ModalSize.MEDIUM} onKeyDown={handleKeyDown} onWheel={handleWheel}> <Modal {...modalProps} size="md" title="Command Palette">
<div> <div className={cl("root")} onKeyDown={handleKeyDown} onWheel={handleWheel}>
<TextInput <TextInput
value={queryEh} value={queryEh}
onChange={e => setQuery(e)} onChange={e => setQuery(e)}
@ -123,7 +123,7 @@ export function CommandPalette({ modalProps }) {
))} ))}
</div> </div>
</div> </div>
</ModalRoot> </Modal>
); );
} }

View file

@ -8,13 +8,13 @@ import "./style.css";
import { ButtonAction } from "@equicordplugins/keyboardNavigation/commands"; import { ButtonAction } from "@equicordplugins/keyboardNavigation/commands";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { RenderModalProps } from "@vencord/discord-types";
import { React, TextInput, useEffect, useState } from "@webpack/common"; import { closeAllModals, Modal,openModal, React, TextInput, useEffect, useState } from "@webpack/common";
import { settings } from ".."; import { settings } from "..";
interface MultipleChoiceProps { interface MultipleChoiceProps {
modalProps: ModalProps; modalProps: RenderModalProps;
onSelect: (selectedValue: any) => void; onSelect: (selectedValue: any) => void;
choices: ButtonAction[]; choices: ButtonAction[];
} }
@ -99,9 +99,8 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice
}, [queryEh]); }, [queryEh]);
return ( return (
// @ts-ignore <Modal {...modalProps} size="md" title="Multiple Choice">
<ModalRoot className={cl("root")} {...modalProps} size={ModalSize.MEDIUM} onKeyDown={handleKeyDown} onWheel={handleWheel}> <div className={cl("root")} onKeyDown={handleKeyDown} onWheel={handleWheel}>
<div>
<TextInput <TextInput
value={queryEh} value={queryEh}
onChange={e => setQuery(e)} onChange={e => setQuery(e)}
@ -123,7 +122,7 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice
))} ))}
</div> </div>
</div> </div>
</ModalRoot> </Modal>
); );
} }

View file

@ -6,11 +6,11 @@
import "./style.css"; import "./style.css";
import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { RenderModalProps } from "@vencord/discord-types";
import { React, TextInput, useEffect, useState } from "@webpack/common"; import { closeAllModals, Modal,openModal, React, TextInput, useEffect, useState } from "@webpack/common";
interface SimpleTextInputProps { interface SimpleTextInputProps {
modalProps: ModalProps; modalProps: RenderModalProps;
onSelect: (inputValue: string) => void; onSelect: (inputValue: string) => void;
placeholder?: string; placeholder?: string;
info?: string; info?: string;
@ -35,16 +35,17 @@ export function SimpleTextInput({ modalProps, onSelect, placeholder, info }: Sim
}, []); }, []);
return ( return (
// @ts-ignore <Modal {...modalProps} size="sm" title="Text Input">
<ModalRoot className="vc-command-palette-simple-text" {...modalProps} size={ModalSize.DYNAMIC} onKeyDown={handleKeyDown}> <div className="vc-command-palette-simple-text" onKeyDown={handleKeyDown}>
<TextInput <TextInput
value={inputValue} value={inputValue}
onChange={e => setInputValue(e as unknown as string)} onChange={e => setInputValue(e as unknown as string)}
style={{ width: "30vw", borderRadius: "5px" }} style={{ width: "30vw", borderRadius: "5px" }}
placeholder={placeholder ?? "Type and press Enter"} placeholder={placeholder ?? "Type and press Enter"}
/> />
{info && <div className="vc-command-palette-textinfo">{info}</div>} {info && <div className="vc-command-palette-textinfo">{info}</div>}
</ModalRoot> </div>
</Modal>
); );
} }

View file

@ -7,10 +7,9 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { classNameFactory } from "@utils/css"; import { classNameFactory } from "@utils/css";
import { closeAllModals } from "@utils/modal";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { SettingsRouter, useState } from "@webpack/common"; import { closeAllModals,SettingsRouter, useState } from "@webpack/common";
import { registerAction } from "./commands"; import { registerAction } from "./commands";
import { openCommandPalette } from "./components/CommandPalette"; import { openCommandPalette } from "./components/CommandPalette";

Some files were not shown because too many files have changed in this diff Show more