From 7e3241e4fc6c5e601e3eecf3aefd5d3f73004c01 Mon Sep 17 00:00:00 2001 From: Ryfter Date: Sat, 25 Jan 2025 11:38:02 -0500 Subject: [PATCH 1/5] Upload files to "/" --- RoleMembersModal.tsx | 234 +++++++++++++++++++++++++++++++++++++------ style.css | 144 ++++++++++++++++++++++---- utils.ts | 11 ++ 3 files changed, 340 insertions(+), 49 deletions(-) create mode 100644 utils.ts diff --git a/RoleMembersModal.tsx b/RoleMembersModal.tsx index 3ef6cb4..de0a4af 100644 --- a/RoleMembersModal.tsx +++ b/RoleMembersModal.tsx @@ -4,40 +4,210 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { Forms, Parser } from "@webpack/common"; -import { GuildMember } from "discord-types/general"; +import { InfoIcon } from "@components/Icons"; +import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { findByCodeLazy, findExportedComponentLazy } from "@webpack"; +import { Constants, GuildChannelStore, GuildMemberStore, GuildStore, Parser, RestAPI, ScrollerThin, Text, Tooltip, useEffect, UserStore, useState } from "@webpack/common"; +import { UnicodeEmoji } from "@webpack/types"; +import type { Role } from "discord-types/general"; -const cl = classNameFactory("vc-inrole-"); +import { cl, GuildUtils } from "./utils"; -export function showInRoleModal(members: GuildMember[], roleId: string, channelId: string) { - openModal(props => - <> - - - - Members of role { - Parser.parse(`<@&${roleId}>`, true, { channelId, viewingChannelId: channelId }) - } ({members.length}) - - - -
- { - members.length !== 0 ? members.map(member => - <> - - {Parser.parse(`<@${member.userId}>`, true, { channelId, viewingChannelId: channelId })} - - - ) : Looks like no online cached members with that role were found. Try scrolling down on your member list to cache more users! - } +type GetRoleIconData = (role: Role, size: number) => { customIconSrc?: string; unicodeEmoji?: UnicodeEmoji; }; +const ThreeDots = findExportedComponentLazy("Dots", "AnimatedDots"); +const getRoleIconData: GetRoleIconData = findByCodeLazy("convertSurrogateToName", "customIconSrc", "unicodeEmoji"); + + + +function getRoleIconSrc(role: Role) { + const icon = getRoleIconData(role, 20); + if (!icon) return; + + const { customIconSrc, unicodeEmoji } = icon; + return customIconSrc ?? unicodeEmoji?.url; +} + +function MembersContainer({ guildId, roleId }: { guildId: string; roleId: string; }) { + + const channelId = GuildChannelStore.getChannels(guildId).SELECTABLE[0].channel.id; + + // RMC: RoleMemberCounts + const [RMC, setRMC] = useState({}); + useEffect(() => { + let loading = true; + const interval = setInterval(async () => { + try { + await RestAPI.get({ + url: Constants.Endpoints.GUILD_ROLE_MEMBER_COUNTS(guildId) + }).then(x => { + if (x.ok) setRMC(x.body); clearInterval(interval); + }); + } catch (error) { console.error("Error fetching member counts", error); } + }, 1000); + return () => { loading = false; }; + }, []); + + let usersInRole = []; + const [rolesFetched, setRolesFetched] = useState(Array); + useEffect(() => { + if (!rolesFetched.includes(roleId)) { + const interval = setInterval(async () => { + try { + const response = await RestAPI.get({ + url: Constants.Endpoints.GUILD_ROLE_MEMBER_IDS(guildId, roleId), + }); + ({ body: usersInRole } = response); + await GuildUtils.requestMembersById(guildId, usersInRole, !1); + setRolesFetched([...rolesFetched, roleId]); + clearInterval(interval); + } catch (error) { console.error("Error fetching members:", error); } + }, 1200); + return () => clearInterval(interval); + } + }, [roleId]); // Fetch roles + + const [members, setMembers] = useState(GuildMemberStore.getMembers(guildId)); + useEffect(() => { + const interval = setInterval(async () => { + if (usersInRole) { + const guildMembers = GuildMemberStore.getMembers(guildId); + const storedIds = guildMembers.map(user => user.userId); + usersInRole.every(id => storedIds.includes(id)) && clearInterval(interval); + if (guildMembers !== members) { + setMembers(GuildMemberStore.getMembers(guildId)); + } + } + }, 500); + return () => clearInterval(interval); + }, [roleId, rolesFetched]); + + const roleMembers = members.filter(x => x.roles.includes(roleId)).map(x => UserStore.getUser(x.userId)); + + return ( +
+
+
+ + {roleMembers.length} loaded / {RMC[roleId] || 0} members with this role
+
+ + {props => } + +
+ +
+ + {roleMembers.map(x => { + return ( +
+ + {Parser.parse(`<@${x.id}>`, true, { channelId, viewingChannelId: channelId })}
- - - - + ); + })} + { + (Object.keys(RMC).length === 0) ? ( +
+ +
+ ) : !RMC[roleId] ? ( + No member found with this role + ) : RMC[roleId] === roleMembers.length ? ( + <> +
+ All members loaded + + ) : rolesFetched.includes(roleId) ? ( + <> +
+ All cached members loaded + + ) : ( +
+ +
+ ) + } + +
); } + +function InRoleModal({ guildId, props, roleId }: { guildId: string; props: ModalProps; roleId: string; }) { + const roleObj = GuildStore.getRoles(guildId); + const roles = Object.keys(roleObj).map(key => roleObj[key]).sort((a, b) => b.position - a.position); + + const [selectedRole, selectRole] = useState(roles.find(x => x.id === roleId) || roles[0]); + + return ( + + + View members with role + + + +
+ + {roles.map((role, index) => { + + if (role.id === guildId) return; + + const roleIconSrc = role != null ? getRoleIconSrc(role) : undefined; + + return ( +
selectRole(roles[index])} + role="button" + tabIndex={0} + key={role.id} + > +
+ + { + roleIconSrc != null && ( + + ) + + } + + {role?.name || "Unknown role"} + +
+
+ ); + })} +
+
+ +
+ + + ); +} + +export function showInRoleModal(guildId: string, roleId: string) { + openModal(props => + + ); +} + diff --git a/style.css b/style.css index b3184ea..1eed2d3 100644 --- a/style.css +++ b/style.css @@ -1,29 +1,139 @@ -.vc-inrole-member-list { - max-height: 400px; - margin-top: 10px; - margin-bottom: 13px; - overflow-x: hidden; +.vc-inrole-modal-content { + padding: 16px 4px 16px 16px; } -.vc-inrole-member-list::-webkit-scrollbar { - background-color: #fff1; - border-radius: 100px; - width: 10px; +.vc-inrole-modal-title { + flex-grow: 1; } -.vc-inrole-member-list::-webkit-scrollbar-thumb { - background-color: #fff3; - border-radius: 100px; +.vc-inrole-modal-container { + width: 100%; + height: 100%; + display: flex; + gap: 8px; } -.vc-inrole-modal-member { - margin: 11px 0; +.vc-inrole-modal-list { + display: flex; + flex-direction: column; + gap: 2px; + padding-right: 8px; + max-width: 300px; + min-width: 300px; } -.vc-inrole-header { - padding-top: "15px"; +.vc-inrole-modal-list-item-btn { + cursor: pointer; } -.vc-inrole-close { +.vc-inrole-modal-list-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + border-radius: 5px; +} + +.vc-inrole-modal-list-item:hover { + background-color: var(--background-modifier-hover); +} + +.vc-inrole-modal-list-item-active { + background-color: var(--background-modifier-selected); +} + +.vc-inrole-modal-list-item > div { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.vc-inrole-modal-role-circle { + border-radius: 50%; + width: 12px; + height: 12px; + flex-shrink: 0; +} + +.vc-inrole-modal-role-image { + width: 20px; + height: 20px; + object-fit: contain; +} + +.vc-inrole-modal-divider { + width: 2px; + background-color: var(--background-modifier-active); +} + +.vc-inrole-role-button { + border-radius: var(--radius-xs); + background: var(--bg-mod-faint); + color: var(--interactive-normal); + border: 1px solid var(--border-faint); + /* stylelint-disable-next-line value-no-vendor-prefix */ + width: -moz-fit-content; + width: fit-content; + height: 24px; + padding: 4px +} + +.custom-profile-theme .vc-inrole-role-button { + background: rgb(var(--bg-overlay-color)/var(--bg-overlay-opacity-6)); + border-color: var(--profile-body-border-color) +} + +.vc-inrole-user-div{ + display: flex; + align-items: center; + gap: 0.2em; +} + +.vc-inrole-modal-members { + display: flex; + flex-direction: column; + width: 100%; +} + + +.vc-inrole-user-avatar { + border-radius: 50%; + padding: 5px; + width: 30px; + height: 30px; +} + +.vc-inrole-member-list-header { + background-color: var(--background-secondary); + padding: 5px; + border-radius: 5px; +} + +.vc-inrole-member-list-header-text { + display: flex; + align-items: center; + gap: 5px; +} + +.vc-inrole-member-list-header-text .vc-info-icon { + color: var(--interactive-muted); margin-left: auto; + cursor: pointer; + transition: color ease-in 0.1s; +} + +.vc-inrole-member-list-header-text .vc-info-icon:hover { + color: var(--interactive-active); +} + +.vc-inrole-member-list-footer { + padding: 5px; + text-align: center; + font-style: italic; +} + +.vc-inrole-divider { + height: 2px; + width: 100%; + background-color: var(--background-modifier-active); } diff --git a/utils.ts b/utils.ts new file mode 100644 index 0000000..b8c765d --- /dev/null +++ b/utils.ts @@ -0,0 +1,11 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classNameFactory } from "@api/Styles"; +import { findByPropsLazy } from "@webpack"; + +export const cl = classNameFactory("vc-inrole-"); +export const GuildUtils = findByPropsLazy("requestMembersById"); -- 2.45.2 From 7f7cf02d45c4c0c982a41632431b35ed2462ad33 Mon Sep 17 00:00:00 2001 From: Ryfter Date: Sat, 25 Jan 2025 11:38:59 -0500 Subject: [PATCH 2/5] Upload files to "/" --- icons.tsx | 17 +++++++++++++++++ index.tsx | 36 +++++++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 icons.tsx diff --git a/icons.tsx b/icons.tsx new file mode 100644 index 0000000..26d432e --- /dev/null +++ b/icons.tsx @@ -0,0 +1,17 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export function MemberIcon() { + return ( + + + + ); +} diff --git a/index.tsx b/index.tsx index 37c0b37..2c57d3d 100644 --- a/index.tsx +++ b/index.tsx @@ -7,14 +7,15 @@ import "./style.css"; import { ApplicationCommandInputType, ApplicationCommandOptionType, sendBotMessage } from "@api/Commands"; +import { findGroupChildrenByChildId } from "@api/ContextMenu"; import { getUserSettingLazy } from "@api/UserSettings"; -import { InfoIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { getCurrentChannel, getCurrentGuild } from "@utils/discord"; import definePlugin from "@utils/types"; import { Forms, GuildMemberStore, GuildStore, Menu, Parser } from "@webpack/common"; -import { GuildMember } from "discord-types/general"; +import { Guild, GuildMember } from "discord-types/general"; +import { MemberIcon } from "./icons"; import { showInRoleModal } from "./RoleMembersModal"; const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!; @@ -33,7 +34,13 @@ function getMembersInRole(roleId: string, guildId: string) { export default definePlugin({ name: "InRole", description: "Know who is in a role with the role context menu or /inrole command (read plugin info!)", - authors: [Devs.nin0dev], + authors: [ + Devs.nin0dev, + { + name: "Ryfter", + id: 898619112350183445n, + }, + ], dependencies: ["UserSettingsAPI"], start() { // DeveloperMode needs to be enabled for the context menu to be shown @@ -44,9 +51,8 @@ export default definePlugin({ <> {Parser.parse(":warning:")} Limitations If you don't have mod permissions on the server, and that server is large (over 100 members), the plugin may be limited in the following ways: - • Offline members won't be listed - • Up to 100 members will be listed by default. To get more, scroll down in the member list to load more members. - • However, friends will always be shown regardless of their status. + • Up to 100 members will be listed by default for each role. To get more, scroll down in the member list to cache more members. + • However, friends will always be shown. ); }, @@ -70,7 +76,7 @@ export default definePlugin({ return sendBotMessage(ctx.channel.id, { content: "Make sure that you are in a server." }); } const role = args[0].value; - showInRoleModal(getMembersInRole(role, ctx.guild.id), role, ctx.channel.id); + showInRoleModal(ctx.guild.id, role); } } ], @@ -90,9 +96,21 @@ export default definePlugin({ id="vc-view-inrole" label="View Members in Role" action={() => { - showInRoleModal(getMembersInRole(role.id, guild.id), role.id, channel.id); + showInRoleModal(guild.id, role.id); }} - icon={InfoIcon} + icon={MemberIcon} + /> + ); + }, + "guild-header-popout"(children, { guild }: { guild: Guild, onClose(): void; }) { + if (!guild) return; + const group = findGroupChildrenByChildId("privacy", children); + group?.push( + showInRoleModal(guild.id, "0")} /> ); } -- 2.45.2 From 1cd541fd67771a764913d2c5d591502260c1d4d1 Mon Sep 17 00:00:00 2001 From: Ryfter Date: Sat, 25 Jan 2025 11:54:52 -0500 Subject: [PATCH 3/5] Added error boundary --- RoleMembersModal.tsx | 97 +++++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/RoleMembersModal.tsx b/RoleMembersModal.tsx index de0a4af..e14f04e 100644 --- a/RoleMembersModal.tsx +++ b/RoleMembersModal.tsx @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import ErrorBoundary from "@components/ErrorBoundary"; import { InfoIcon } from "@components/Icons"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { findByCodeLazy, findExportedComponentLazy } from "@webpack"; @@ -144,60 +145,62 @@ function InRoleModal({ guildId, props, roleId }: { guildId: string; props: Modal const [selectedRole, selectRole] = useState(roles.find(x => x.id === roleId) || roles[0]); return ( - - - View members with role - - - -
- - {roles.map((role, index) => { + + + + View members with role + + + +
+ + {roles.map((role, index) => { - if (role.id === guildId) return; + if (role.id === guildId) return; - const roleIconSrc = role != null ? getRoleIconSrc(role) : undefined; + const roleIconSrc = role != null ? getRoleIconSrc(role) : undefined; - return ( -
selectRole(roles[index])} - role="button" - tabIndex={0} - key={role.id} - > + return (
selectRole(roles[index])} + role="button" + tabIndex={0} + key={role.id} > - - { - roleIconSrc != null && ( - - ) +
+ + { + roleIconSrc != null && ( + + ) - } - - {role?.name || "Unknown role"} - + } + + {role?.name || "Unknown role"} + +
-
- ); - })} -
-
- -
- - + ); + })} + +
+ +
+ + + ); } -- 2.45.2 From 56d5b10e1b9267d44665436f103f147784bf6237 Mon Sep 17 00:00:00 2001 From: Ryfter Date: Sat, 25 Jan 2025 14:00:44 -0500 Subject: [PATCH 4/5] Added 1s delay to stop ratelimits even more added a 1s cooldown between each role selection to stop ratelimits, except if the role has already been fetched --- RoleMembersModal.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/RoleMembersModal.tsx b/RoleMembersModal.tsx index e14f04e..76c5756 100644 --- a/RoleMembersModal.tsx +++ b/RoleMembersModal.tsx @@ -8,7 +8,7 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { InfoIcon } from "@components/Icons"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { findByCodeLazy, findExportedComponentLazy } from "@webpack"; -import { Constants, GuildChannelStore, GuildMemberStore, GuildStore, Parser, RestAPI, ScrollerThin, Text, Tooltip, useEffect, UserStore, useState } from "@webpack/common"; +import { Constants, GuildChannelStore, GuildMemberStore, GuildStore, Parser, RestAPI, ScrollerThin, showToast, Text, Tooltip, useEffect, UserStore, useState } from "@webpack/common"; import { UnicodeEmoji } from "@webpack/types"; import type { Role } from "discord-types/general"; @@ -18,7 +18,8 @@ type GetRoleIconData = (role: Role, size: number) => { customIconSrc?: string; u const ThreeDots = findExportedComponentLazy("Dots", "AnimatedDots"); const getRoleIconData: GetRoleIconData = findByCodeLazy("convertSurrogateToName", "customIconSrc", "unicodeEmoji"); - +let rolesFetched2; +let members2; function getRoleIconSrc(role: Role) { const icon = getRoleIconData(role, 20); @@ -60,6 +61,7 @@ function MembersContainer({ guildId, roleId }: { guildId: string; roleId: string ({ body: usersInRole } = response); await GuildUtils.requestMembersById(guildId, usersInRole, !1); setRolesFetched([...rolesFetched, roleId]); + rolesFetched2 = rolesFetched; clearInterval(interval); } catch (error) { console.error("Error fetching members:", error); } }, 1200); @@ -144,6 +146,12 @@ function InRoleModal({ guildId, props, roleId }: { guildId: string; props: Modal const [selectedRole, selectRole] = useState(roles.find(x => x.id === roleId) || roles[0]); + let cooldown; + useEffect(() => { + const timeout = setTimeout(() => cooldown = false, 1000); + return () => clearTimeout(timeout); + }, [selectedRole]); + return ( @@ -163,7 +171,11 @@ function InRoleModal({ guildId, props, roleId }: { guildId: string; props: Modal return (
selectRole(roles[index])} + onClick={() => { + if (selectedRole.id === roles[index].id) return; + cooldown && !rolesFetched2.includes(roles[index].id) ? showToast("To limit ratelimiting, please wait at least a second before switching roles.") + : (selectRole(roles[index]), cooldown = true); + }} role="button" tabIndex={0} key={role.id} -- 2.45.2 From f4e0f54fab9baf1536c80593242a0e164bbdf90f Mon Sep 17 00:00:00 2001 From: Ryfter Date: Sat, 25 Jan 2025 14:18:41 -0500 Subject: [PATCH 5/5] Removed unused function --- index.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/index.tsx b/index.tsx index 2c57d3d..7d858d5 100644 --- a/index.tsx +++ b/index.tsx @@ -20,17 +20,6 @@ import { showInRoleModal } from "./RoleMembersModal"; const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!; -function getMembersInRole(roleId: string, guildId: string) { - const members = GuildMemberStore.getMembers(guildId); - const membersInRole: GuildMember[] = []; - members.forEach(member => { - if (member.roles.includes(roleId)) { - membersInRole.push(member); - } - }); - return membersInRole; -} - export default definePlugin({ name: "InRole", description: "Know who is in a role with the role context menu or /inrole command (read plugin info!)", -- 2.45.2