diff --git a/componenents/ViewMembersModal.tsx b/componenents/ViewMembersModal.tsx deleted file mode 100644 index fb0dc0b..0000000 --- a/componenents/ViewMembersModal.tsx +++ /dev/null @@ -1,213 +0,0 @@ -/* - * 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/components/ViewMembersModal.tsx b/components/ViewMembersModal.tsx new file mode 100644 index 0000000..810d5f1 --- /dev/null +++ b/components/ViewMembersModal.tsx @@ -0,0 +1,326 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * 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, findComponentByCodeLazy } from "@webpack"; +import { Constants, GuildChannelStore, GuildMemberStore, GuildRoleStore, Parser, RestAPI, ScrollerThin, Text, Tooltip, useEffect, useRef, UserStore, useState } from "@webpack/common"; + +import { cl, GuildUtils } from "../utils"; + +type GetRoleIconData = (role: any, size: number) => { customIconSrc?: string; unicodeEmoji?: any; }; +const ThreeDots = findComponentByCodeLazy(".dots,", "dotRadius:"); +const getRoleIconData: GetRoleIconData = findByCodeLazy("convertSurrogateToName", "customIconSrc", "unicodeEmoji"); + +function LoadingDots({ dotRadius, themed }: { dotRadius: number; themed: boolean; }) { + return ( + Loading...}> + + + ); +} + + + +function getRoleIconSrc(role: any) { + const icon = getRoleIconData(role, 20); + if (!icon) return; + + const { customIconSrc, unicodeEmoji } = icon; + return customIconSrc ?? unicodeEmoji?.url; +} + +function RoleCircle({ color }: { color?: string; }) { + const ref = useRef(null); + + useEffect(() => { + if (ref.current) { + ref.current.style.setProperty("--vc-role-color", color || "var(--primary-300)"); + } + }, [color]); + + return ; +} + +function MembersContainer({ guildId, roleId }: { guildId: string; roleId: string; }) { + // Safely get channelId, fallback to guildId if no selectable channels + const selectableChannels = GuildChannelStore.getChannels(guildId)?.SELECTABLE; + const channelId = selectableChannels?.[0]?.channel?.id || guildId; + + // RMC: RoleMemberCounts - Try API endpoint first, fallback to calculating from members + const [RMC, setRMC] = useState>({}); + const [apiCountsLoaded, setApiCountsLoaded] = useState(false); + + useEffect(() => { + let cancelled = false; + + // Try to fetch from API endpoint if it exists + const tryFetchCounts = async () => { + if (cancelled || apiCountsLoaded) return; + + try { + // Check if endpoint exists before calling + if (Constants.Endpoints.GUILD_ROLE_MEMBER_COUNTS) { + const response = await RestAPI.get({ + url: Constants.Endpoints.GUILD_ROLE_MEMBER_COUNTS(guildId) + }); + if (response.ok && response.body) { + setRMC(response.body); + setApiCountsLoaded(true); + return; + } + } + } catch (error) { + // Endpoint doesn't exist or failed, will calculate from members + } + + // Fallback: Calculate counts from cached members + const members = GuildMemberStore.getMembers(guildId); + const roleCounts: Record = {}; + members.forEach(member => { + if (member?.roles) { + member.roles.forEach(role => { + roleCounts[role] = (roleCounts[role] || 0) + 1; + }); + } + }); + setRMC(roleCounts); + setApiCountsLoaded(true); + }; + + tryFetchCounts(); + + return () => { + cancelled = true; + }; + }, [guildId, apiCountsLoaded]); + + const [usersInRole, setUsersInRole] = useState([]); + const [rolesFetched, setRolesFetched] = useState>(new Set()); + const [fetchError, setFetchError] = useState(null); + + useEffect(() => { + if (rolesFetched.has(roleId)) return; + + let cancelled = false; + + const tryFetchMemberIds = async () => { + if (cancelled) return; + + try { + // Try API endpoint if it exists + if (Constants.Endpoints.GUILD_ROLE_MEMBER_IDS) { + const response = await RestAPI.get({ + url: Constants.Endpoints.GUILD_ROLE_MEMBER_IDS(guildId, roleId), + }); + if (response.ok && response.body) { + const memberIds: string[] = response.body || []; + if (memberIds.length > 0) { + await GuildUtils.requestMembersById(guildId, memberIds, false); + setUsersInRole(memberIds); + } + setRolesFetched(prev => new Set([...prev, roleId])); + setFetchError(null); + return; + } + } + } catch (error) { + // API endpoint doesn't exist or failed, will use fallback + setFetchError("API endpoint not available, using cached members"); + } + + // Fallback: Use cached members and request more if needed + const members = GuildMemberStore.getMembers(guildId); + const roleMemberIds = members + .filter(m => m?.roles?.includes(roleId)) + .map(m => m.userId); + + setUsersInRole(roleMemberIds); + setRolesFetched(prev => new Set([...prev, roleId])); + }; + + const timeout = setTimeout(tryFetchMemberIds, 100); + + return () => { + cancelled = true; + clearTimeout(timeout); + }; + }, [guildId, roleId, rolesFetched]); + + const [members, setMembers] = useState(GuildMemberStore.getMembers(guildId)); + useEffect(() => { + let cancelled = false; + const interval = setInterval(() => { + if (cancelled) return; + const guildMembers = GuildMemberStore.getMembers(guildId); + + if (guildMembers !== members) { + setMembers(guildMembers); + + // Update role counts when members change + if (!apiCountsLoaded) { + const roleCounts: Record = {}; + guildMembers.forEach(member => { + if (member?.roles) { + member.roles.forEach(role => { + roleCounts[role] = (roleCounts[role] || 0) + 1; + }); + } + }); + setRMC(prev => ({ ...prev, ...roleCounts })); + } + } + }, 500); + return () => { + cancelled = true; + clearInterval(interval); + }; + }, [guildId, members, apiCountsLoaded]); + + const roleMembers = members + .filter(x => x?.roles?.includes(roleId)) + .map(x => UserStore.getUser(x.userId)) + .filter(x => x != null); + + return ( +
+
+
+ + {roleMembers.length} loaded / {RMC[roleId] || 0} members with this role
+
+ + {props => } + +
+ +
+ + {roleMembers.map(x => { + if (!x) return null; + return ( +
+ {x.username + {Parser.parse(`<@${x.id}>`, true, { channelId, viewingChannelId: channelId })} +
+ ); + })} + { + (Object.keys(RMC).length === 0 && !apiCountsLoaded) ? ( +
+ +
+ ) : !RMC[roleId] && roleMembers.length === 0 ? ( + No member found with this role + ) : RMC[roleId] && RMC[roleId] === roleMembers.length ? ( + <> +
+ All members loaded + + ) : rolesFetched.has(roleId) ? ( + <> +
+ + {fetchError ? `${roleMembers.length} cached members loaded` : "All cached members loaded"} + + {fetchError && ( + + {fetchError} + + )} + + ) : ( +
+ +
+ ) + } + +
+ ); +} + +function VMWRModal({ guildId, props }: { guildId: string; props: ModalProps; }) { + const roleObj = GuildRoleStore.getRolesSnapshot(guildId); + const roles = Object.keys(roleObj).map(key => roleObj[key]).sort((a, b) => b.position - a.position); + + const [selectedRole, selectRole] = useState(roles[0] || null); + + 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 + ) + + } + + {role?.name || "Unknown role"} + +
+
+ ); + })} +
+
+ {selectedRole && ( + + )} +
+ + + ); +} + +export function openVMWRModal(guildId) { + + openModal(props => + + ); +} + diff --git a/componenents/icons.tsx b/components/icons.tsx similarity index 94% rename from componenents/icons.tsx rename to components/icons.tsx index 26d432e..12ff946 100644 --- a/componenents/icons.tsx +++ b/components/icons.tsx @@ -1,6 +1,6 @@ /* * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors + * Copyright (c) 2025 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -15,3 +15,4 @@ export function MemberIcon() { ); } + diff --git a/index.tsx b/index.tsx index e38d402..e449159 100644 --- a/index.tsx +++ b/index.tsx @@ -1,6 +1,6 @@ /* * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors + * Copyright (c) 2025 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -8,11 +8,11 @@ import "./styles.css"; import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import definePlugin from "@utils/types"; +import type { Guild } from "@vencord/discord-types"; import { Menu } from "@webpack/common"; -import type { Guild } from "discord-types/general"; -import { MemberIcon } from "./componenents/icons"; -import { openVMWRModal } from "./componenents/ViewMembersModal"; +import { MemberIcon } from "./components/icons"; +import { openVMWRModal } from "./components/ViewMembersModal"; // VMWR: View Members With Role const makeContextMenuPatch: () => NavContextMenuPatchCallback = () => (children, { guild }: { guild: Guild, onClose(): void; }) => { diff --git a/styles.css b/styles.css index c02bb90..db3212f 100644 --- a/styles.css +++ b/styles.css @@ -53,6 +53,7 @@ width: 12px; height: 12px; flex-shrink: 0; + background-color: var(--vc-role-color, var(--primary-300)); } .vc-vmwr-modal-role-image { @@ -75,7 +76,7 @@ width: -moz-fit-content; width: fit-content; height: 24px; - padding: 4px + padding: 4px; } .custom-profile-theme .vc-vmwr-role-button { @@ -83,7 +84,7 @@ border-color: var(--profile-body-border-color) } -.vc-vmwr-user-div{ +.vc-vmwr-user-div { display: flex; align-items: center; gap: 0.2em; diff --git a/utils.ts b/utils.ts index 1fbaea6..a58ca5f 100644 --- a/utils.ts +++ b/utils.ts @@ -1,6 +1,6 @@ /* * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors + * Copyright (c) 2025 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */