diff --git a/src/plugins/viewMembersWithRole/README.md b/src/plugins/viewMembersWithRole/README.md new file mode 100644 index 00000000000..def2f0e5e89 --- /dev/null +++ b/src/plugins/viewMembersWithRole/README.md @@ -0,0 +1,17 @@ +# viewMembersWithRole + +### Installation +Check [this](https://docs.vencord.dev/installing/custom-plugins/) to install + +### Usage +Open server context menu and select View members with role + +image + +Now select the role you want +![image](https://github.com/user-attachments/assets/d74aec13-cdcf-4170-9c40-0d12853bf600) + + +### Limitations +It will only show the first hundred members per role, as well as the already cached members. + diff --git a/src/plugins/viewMembersWithRole/componenents/ViewMembersModal.tsx b/src/plugins/viewMembersWithRole/componenents/ViewMembersModal.tsx new file mode 100644 index 00000000000..fb0dc0b2243 --- /dev/null +++ b/src/plugins/viewMembersWithRole/componenents/ViewMembersModal.tsx @@ -0,0 +1,213 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +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"; + +import { cl, GuildUtils } from "../utils"; + +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 VMWRModal({ guildId, props }: { guildId: string; props: ModalProps; }) { + 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[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 openVMWRModal(guildId) { + + openModal(props => + + ); +} + diff --git a/src/plugins/viewMembersWithRole/componenents/icons.tsx b/src/plugins/viewMembersWithRole/componenents/icons.tsx new file mode 100644 index 00000000000..26d432e218e --- /dev/null +++ b/src/plugins/viewMembersWithRole/componenents/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/src/plugins/viewMembersWithRole/index.tsx b/src/plugins/viewMembersWithRole/index.tsx new file mode 100644 index 00000000000..72ac0ad0444 --- /dev/null +++ b/src/plugins/viewMembersWithRole/index.tsx @@ -0,0 +1,44 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { Menu } from "@webpack/common"; +import type { Guild } from "discord-types/general"; + +import { MemberIcon } from "./componenents/icons"; +import { openVMWRModal } from "./componenents/ViewMembersModal"; + +// VMWR: View Members With Role +const makeContextMenuPatch: () => NavContextMenuPatchCallback = () => (children, { guild }: { guild: Guild, onClose(): void; }) => { + if (!guild) return; + + const group = findGroupChildrenByChildId("privacy", children); + group?.push( + openVMWRModal(guild.id)} + /> + ); +}; + +export default definePlugin({ + name: "ViewMembersWithRole", + description: "Shows all the members with the selected roles", + authors: [ + Devs.Ryfter + ], + contextMenus: { + "guild-header-popout": makeContextMenuPatch() + }, + start() { }, + stop() { }, +}); diff --git a/src/plugins/viewMembersWithRole/styles.css b/src/plugins/viewMembersWithRole/styles.css new file mode 100644 index 00000000000..c02bb90f025 --- /dev/null +++ b/src/plugins/viewMembersWithRole/styles.css @@ -0,0 +1,139 @@ +.vc-vmwr-modal-content { + padding: 16px 4px 16px 16px; +} + +.vc-vmwr-modal-title { + flex-grow: 1; +} + +.vc-vmwr-modal-container { + width: 100%; + height: 100%; + display: flex; + gap: 8px; +} + +.vc-vmwr-modal-list { + display: flex; + flex-direction: column; + gap: 2px; + padding-right: 8px; + max-width: 300px; + min-width: 300px; +} + +.vc-vmwr-modal-list-item-btn { + cursor: pointer; +} + +.vc-vmwr-modal-list-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + border-radius: 5px; +} + +.vc-vmwr-modal-list-item:hover { + background-color: var(--background-modifier-hover); +} + +.vc-vmwr-modal-list-item-active { + background-color: var(--background-modifier-selected); +} + +.vc-vmwr-modal-list-item > div { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.vc-vmwr-modal-role-circle { + border-radius: 50%; + width: 12px; + height: 12px; + flex-shrink: 0; +} + +.vc-vmwr-modal-role-image { + width: 20px; + height: 20px; + object-fit: contain; +} + +.vc-vmwr-modal-divider { + width: 2px; + background-color: var(--background-modifier-active); +} + +.vc-vmwr-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-vmwr-role-button { + background: rgb(var(--bg-overlay-color)/var(--bg-overlay-opacity-6)); + border-color: var(--profile-body-border-color) +} + +.vc-vmwr-user-div{ + display: flex; + align-items: center; + gap: 0.2em; +} + +.vc-vmwr-modal-members { + display: flex; + flex-direction: column; + width: 100%; +} + + +.vc-vmwr-user-avatar { + border-radius: 50%; + padding: 5px; + width: 30px; + height: 30px; +} + +.vc-vmwr-member-list-header { + background-color: var(--background-secondary); + padding: 5px; + border-radius: 5px; +} + +.vc-vmwr-member-list-header-text { + display: flex; + align-items: center; + gap: 5px; +} + +.vc-vmwr-member-list-header-text .vc-info-icon { + color: var(--interactive-muted); + margin-left: auto; + cursor: pointer; + transition: color ease-in 0.1s; +} + +.vc-vmwr-member-list-header-text .vc-info-icon:hover { + color: var(--interactive-active); +} + +.vc-vmwr-member-list-footer { + padding: 5px; + text-align: center; + font-style: italic; +} + +.vc-vmwr-divider { + height: 2px; + width: 100%; + background-color: var(--background-modifier-active); +} diff --git a/src/plugins/viewMembersWithRole/utils.ts b/src/plugins/viewMembersWithRole/utils.ts new file mode 100644 index 00000000000..1fbaea65332 --- /dev/null +++ b/src/plugins/viewMembersWithRole/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-vmwr-"); +export const GuildUtils = findByPropsLazy("requestMembersById"); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e7582591257..75affe397d6 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -575,6 +575,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "SomeAspy", id: 516750892372852754n, }, + Ryfter: { + name: "Ryfter", + id: 898619112350183445n, + }, jamesbt365: { name: "jamesbt365", id: 158567567487795200n,