diff --git a/package.json b/package.json index b6eefbeda..92a4a33f5 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@ffmpeg/util": "^0.12.1", "@fluent/langneg": "^0.7.0", "@intrnl/xxhash64": "^0.1.2", - "@song-spotlight/api": "^2.0.0", + "@song-spotlight/api": "^2.0.1", "@streamparser/json": "^0.0.22", "@types/less": "^3.0.8", "@types/stylus": "^0.48.42", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5eb88324..66d178d36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,8 +29,8 @@ importers: specifier: ^0.1.2 version: 0.1.2 '@song-spotlight/api': - specifier: ^2.0.0 - version: 2.0.0(typescript@5.9.3) + specifier: ^2.0.1 + version: 2.0.1(typescript@5.9.3) '@streamparser/json': specifier: ^0.0.22 version: 0.0.22 @@ -728,8 +728,8 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@song-spotlight/api@2.0.0': - resolution: {integrity: sha512-a3gyrSojd3BzwloxZHyDXA7HhjmCNQZOYATh7AZJyCXIqilBMwFPgQ2FVkUpzf6MzZhN9emeN6OGZOJb/rkECw==} + '@song-spotlight/api@2.0.1': + resolution: {integrity: sha512-nxBJch8cGp10NYeJSp3DCPS/LYmlpcs9VCHrHzgCHFRICTKWhlpiUZgrACP9TmQ8RQsE4B9e23BcQDaqWqPCGg==} peerDependencies: typescript: ^5 @@ -948,6 +948,10 @@ packages: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -964,6 +968,10 @@ packages: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + array.prototype.flatmap@1.3.3: resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} engines: {node: '>= 0.4'} @@ -1265,6 +1273,10 @@ packages: resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} engines: {node: '>= 0.4'} + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1329,8 +1341,8 @@ packages: eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - eslint-module-utils@2.12.0: - resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -1764,6 +1776,10 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -1954,6 +1970,9 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -2224,6 +2243,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -2347,6 +2371,10 @@ packages: standalone-electron-types@34.2.0: resolution: {integrity: sha512-+BIrNe0TdZBBBRS3G/F7cPbuBipSZylSjSrJu9+sjw84+vz36a5oZ7l9NeH7aXgMWmPzPZKsTm+K0nOIvhENJQ==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + streamx@2.22.0: resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==} @@ -2595,6 +2623,10 @@ packages: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -3015,7 +3047,7 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@song-spotlight/api@2.0.0(typescript@5.9.3)': + '@song-spotlight/api@2.0.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 optionalDependencies: @@ -3280,6 +3312,17 @@ snapshots: get-intrinsic: 1.3.0 is-string: 1.1.1 + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + array-union@2.1.0: {} array.prototype.findlast@1.2.5: @@ -3296,7 +3339,7 @@ snapshots: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 @@ -3308,6 +3351,13 @@ snapshots: es-abstract: 1.23.9 es-shim-unscopables: 1.1.0 + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 @@ -3653,6 +3703,63 @@ snapshots: unbox-primitive: 1.1.0 which-typed-array: 1.1.19 + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -3776,11 +3883,11 @@ snapshots: dependencies: debug: 3.2.7 is-core-module: 2.16.1 - resolve: 1.22.8 + resolve: 1.22.11 transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -3793,19 +3900,19 @@ snapshots: eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1): dependencies: '@rtsao/scc': 1.1.0 - array-includes: 3.1.8 + array-includes: 3.1.9 array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.2 + array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 eslint: 9.39.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.1.5 object.fromentries: 2.0.8 object.groupby: 1.0.3 object.values: 1.2.1 @@ -4285,6 +4392,8 @@ snapshots: is-map@2.0.3: {} + is-negative-zero@2.0.3: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -4459,6 +4568,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -4533,7 +4646,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.1 object.values@1.2.1: dependencies: @@ -4743,6 +4856,12 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + resolve@1.22.8: dependencies: is-core-module: 2.16.1 @@ -4884,6 +5003,11 @@ snapshots: dependencies: '@types/node': 22.13.13 + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + streamx@2.22.0: dependencies: fast-fifo: 1.3.2 @@ -5248,6 +5372,16 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@1.3.1: dependencies: isexe: 2.0.0 diff --git a/src/equicordplugins/declutter/index.tsx b/src/equicordplugins/declutter/index.tsx index ab0b8e503..2a74a23b1 100644 --- a/src/equicordplugins/declutter/index.tsx +++ b/src/equicordplugins/declutter/index.tsx @@ -4,10 +4,15 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import "./style.css"; + import { definePluginSettings, migratePluginSetting, migratePluginSettings } from "@api/Settings"; +import { Divider } from "@components/Divider"; import { HeadingSecondary } from "@components/Heading"; +import { Notice } from "@components/Notice"; +import { classNameFactory } from "@utils/css"; import { Devs, EquicordDevs } from "@utils/index"; -import { t } from "@utils/translation"; +import { t, Translate } from "@utils/translation"; import definePlugin, { OptionType } from "@utils/types"; migratePluginSettings("Declutter", "BetterUserArea", "Anammox"); @@ -25,6 +30,8 @@ for (const [oldKey, newKey] of migrationsAnammox) { migratePluginSetting("Declutter", newKey, oldKey); } +const cl = classNameFactory("vc-declutter-"); + export const settings = definePluginSettings({ userProfileHeader: { type: OptionType.COMPONENT, @@ -60,11 +67,15 @@ export const settings = definePluginSettings({ default: true, restartNeeded: true }, - removeUsernameStyles: { - type: OptionType.BOOLEAN, - description: t("declutter.settings.removeUsernameStyles"), - default: true, - restartNeeded: true + accessibilityNotice: { + type: OptionType.COMPONENT, + component: () => ( + + + Discord already has a built-in username style option in Accessibility settings. + + + ) }, friendsListHeader: { type: OptionType.COMPONENT, @@ -73,7 +84,7 @@ export const settings = definePluginSettings({ removeShopAboveDM: { type: OptionType.BOOLEAN, description: t("declutter.settings.removeShopAboveDM"), - default: true, + default: false, restartNeeded: true, }, removeQuestsAboveDM: { @@ -126,11 +137,12 @@ export const settings = definePluginSettings({ function SectionSeparator(title: string) { return ( - <> -
- {title} -
- +
+ + + {title} + +
); } @@ -177,20 +189,13 @@ export default definePlugin({ predicate: () => settings.store.removeClanTag, }, { - // Username styles and allways show username + // Always show username find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));", - replacement: [ - { - match: /displayNameStyles:(\i),/, - replace: "displayNameStyles:void 0,", - predicate: () => settings.store.removeUsernameStyles - }, - { - match: /hoverText:(\i),forceHover:\i,children:/g, - replace: "hoverText:$1,forceHover:!0,children:", - predicate: () => settings.store.alwaysShowUsername - }, - ], + replacement: { + match: /hoverText:(\i),forceHover:\i,children:/g, + replace: "hoverText:$1,forceHover:!0,children:" + }, + predicate: () => settings.store.alwaysShowUsername }, { // Button tooltips in user area diff --git a/src/equicordplugins/declutter/style.css b/src/equicordplugins/declutter/style.css new file mode 100644 index 000000000..f979e2235 --- /dev/null +++ b/src/equicordplugins/declutter/style.css @@ -0,0 +1,19 @@ +.vc-declutter-accessibility-notice { + margin-top: 8px; + margin-bottom: 8px; +} + +.vc-declutter-section-separator { + margin-top: 12px; + margin-bottom: 6px; +} + +.vc-declutter-section-title { + margin-top: 14px; + margin-bottom: 0; + font-size: 16px; + line-height: 20px; + text-transform: uppercase; + letter-spacing: 0.02em; + color: var(--text-muted); +} diff --git a/src/equicordplugins/voiceButtons/index.tsx b/src/equicordplugins/voiceButtons/index.tsx index 8edbcc01e..a59b55aa1 100644 --- a/src/equicordplugins/voiceButtons/index.tsx +++ b/src/equicordplugins/voiceButtons/index.tsx @@ -25,7 +25,7 @@ export default definePlugin({ replacement: [ { match: /\[\i\.\i\]:\i\}\),children:\[/, - replace: "$&$self.renderButtons(arguments[0].user)," + replace: "$&$self.renderButtons(arguments[0]?.user)," } ] } diff --git a/src/plugins/_api/nicknameIcons.ts b/src/plugins/_api/nicknameIcons.ts index 47c7b8905..81423e450 100644 --- a/src/plugins/_api/nicknameIcons.ts +++ b/src/plugins/_api/nicknameIcons.ts @@ -15,7 +15,7 @@ export default definePlugin({ { find: "#{intl::USER_PROFILE_PRONOUNS}", replacement: { - match: /(?<=shouldUnderlineOnHover:null.{0,150})children:(\i)/, + match: /(?<=shouldUnderlineOnHover:null.{0,200})children:(\i)/, replace: "children:[...Vencord.Api.NicknameIcons._renderIcons({userId:arguments[0].user?.id}),$1]" } } diff --git a/src/plugins/clickableRoles/index.tsx b/src/plugins/clickableRoles/index.tsx new file mode 100644 index 000000000..8f86916c0 --- /dev/null +++ b/src/plugins/clickableRoles/index.tsx @@ -0,0 +1,187 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./style.css"; + +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; +import { openUserProfile } from "@utils/discord"; +import { isTruthy } from "@utils/guards"; +import { Logger } from "@utils/Logger"; +import definePlugin from "@utils/types"; +import type { Role } from "@vencord/discord-types"; +import { findByPropsLazy } from "@webpack"; +import { Constants, GuildRoleStore, IconUtils, Popout, RestAPI, ScrollerThin, useEffect, useRef, UserStore, useState, useStateFromStores } from "@webpack/common"; + +const logger = new Logger("ClickableRoles"); + +const cl = classNameFactory("vc-clickableroles-"); + +const GuildActions = findByPropsLazy("requestMembersById", "banUser"); + +const MAX_VISIBLE_MEMBERS = 20; +const CACHE_TTL = 60_000; + +const memberCache = new Map(); + +function getCachedMemberIds(guildId: string, roleId: string) { + const key = `${guildId}-${roleId}`; + const cached = memberCache.get(key); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) return cached.ids; + return null; +} + +function setCachedMemberIds(guildId: string, roleId: string, ids: string[]) { + memberCache.set(`${guildId}-${roleId}`, { ids, timestamp: Date.now() }); +} + +function RoleMembersList({ roleId, guildId, closePopout, setPopoutRef }: { roleId: string; guildId: string; closePopout(): void; setPopoutRef(ref: HTMLDivElement | null): void; }) { + const [memberIds, setMemberIds] = useState([]); + const [loading, setLoading] = useState(true); + + const role = useStateFromStores([GuildRoleStore], () => GuildRoleStore.getRole(guildId, roleId)); + + const [totalCount, setTotalCount] = useState(0); + + useEffect(() => { + let cancelled = false; + + const cached = getCachedMemberIds(guildId, roleId); + if (cached) { + setTotalCount(cached.length); + const visible = cached.slice(0, MAX_VISIBLE_MEMBERS); + setMemberIds(visible); + if (visible.length) GuildActions.requestMembersById(guildId, visible, false); + setLoading(false); + } else { + RestAPI.get({ + url: Constants.Endpoints.GUILD_ROLE_MEMBER_IDS(guildId, roleId), + }).then(res => { + if (cancelled) return; + const ids = res.body as string[]; + setCachedMemberIds(guildId, roleId, ids); + setTotalCount(ids.length); + const visible = ids.slice(0, MAX_VISIBLE_MEMBERS); + setMemberIds(visible); + if (visible.length) GuildActions.requestMembersById(guildId, visible, false); + setLoading(false); + }).catch(e => { + if (cancelled) return; + logger.error("Failed to fetch role members", e); + setLoading(false); + }); + } + + return () => { cancelled = true; }; + }, [guildId, roleId]); + + const users = useStateFromStores( + [UserStore], + () => memberIds.map(id => UserStore.getUser(id)).filter(isTruthy), + [memberIds] + ); + + return ( +
+
+ + {role?.name ?? "Unknown Role"} + {totalCount} +
+ + {loading ? ( +
Loading members...
+ ) : users.length === 0 ? ( +
No members found.
+ ) : users.map(user => ( +
{ + closePopout(); + openUserProfile(user.id); + }} + > + + {user.globalName ?? user.username} +
+ ))} + {totalCount > MAX_VISIBLE_MEMBERS && ( +
and {totalCount - MAX_VISIBLE_MEMBERS} more
+ )} +
+
+ ); +} + +function ClickableRole({ roleId, guildId, children }: { roleId: string; guildId: string; children: React.ReactNode; }) { + const ref = useRef(null); + + return ( + ( + + )} + > + {popoutProps => ( +
+ {children} +
+ )} +
+ ); +} + +const WrappedClickableRole = ErrorBoundary.wrap(ClickableRole, { noop: true }); + +export default definePlugin({ + name: "ClickableRoles", + description: "Click on roles in user profiles and the member list to see which members have them.", + authors: [Devs.prism], + + patches: [ + { + find: "#{intl::T+3Adg::raw}", + replacement: { + match: /(colors:\i\}\);)return(.*?enableTooltip:!1\}\):null,\i\]\}\))/, + replace: "$1return $self.wrapRolePill(arguments[0],()=>$2)", + }, + }, + { + find: 'tutorialId:"whos-online"', + replacement: { + match: /\((function\(\i\)\{let\{id:.*?"\\xa0— ".{0,15})\}\);/, + replace: "($self.wrapRoleGroup($1}));", + }, + }, + ], + + wrapRoleGroup(originalFn: (props: { id: string; guildId: string; }) => React.ReactNode) { + return (props: { id: string; guildId: string; }) => { + const result = originalFn(props); + if (!GuildRoleStore.getRole(props.guildId, props.id)) return result; + return ( + + {result} + + ); + }; + }, + + wrapRolePill(props: { role: Role; guildId: string; }, renderOriginal: () => React.ReactNode) { + return ( + + {renderOriginal()} + + ); + }, +}); diff --git a/src/plugins/clickableRoles/style.css b/src/plugins/clickableRoles/style.css new file mode 100644 index 000000000..59a1ce523 --- /dev/null +++ b/src/plugins/clickableRoles/style.css @@ -0,0 +1,80 @@ +.vc-clickableroles-trigger { + cursor: pointer; +} + +.vc-clickableroles-popout { + background-color: var(--background-surface-highest); + border-radius: 8px; + padding: 12px; + width: 250px; + max-height: var(--reference-position-layer-max-height, 350px); + box-sizing: border-box; + display: flex; + flex-direction: column; +} + +.vc-clickableroles-list { + max-height: 260px; + min-height: 0; + flex: 1; +} + +.vc-clickableroles-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 8px; +} + +.vc-clickableroles-color { + width: 12px; + height: 12px; + border-radius: 50%; + flex-shrink: 0; +} + +.vc-clickableroles-name { + font-weight: 600; + color: var(--interactive-text-default); +} + +.vc-clickableroles-count { + color: var(--text-muted); + margin-left: auto; + font-size: 12px; +} + +.vc-clickableroles-empty { + padding: 8px; + text-align: center; + color: var(--text-muted); + font-size: 13px; +} + +.vc-clickableroles-user { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 6px; + cursor: pointer; + border-radius: 4px; + color: var(--interactive-text-default); + font-size: 14px; +} + +.vc-clickableroles-user:hover { + background-color: var(--interactive-background-hover); +} + +.vc-clickableroles-avatar { + width: 24px; + height: 24px; + border-radius: 50%; +} + +.vc-clickableroles-overflow { + padding: 6px; + text-align: center; + color: var(--text-muted); + font-size: 12px; +}