/* * Vencord, a Discord client mod * Copyright (c) 2024 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, findExportedComponentLazy } from "@webpack"; 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"; 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"); let rolesFetched2; let members2; 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<string>); 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]); rolesFetched2 = rolesFetched; 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 ( <div className={cl("modal-members")}> <div className={cl("member-list-header")}> <div className={cl("member-list-header-text")}> <Text> {roleMembers.length} loaded / {RMC[roleId] || 0} members with this role<br /> </Text> <Tooltip text="For roles with over 100 members, only the first 100 and the cached members will be shown."> {props => <InfoIcon {...props} />} </Tooltip> </div> </div> <ScrollerThin orientation="auto"> {roleMembers.map(x => { return ( <div key={x.id} className={cl("user-div")}> <img className={cl("user-avatar")} src={x.getAvatarURL()} alt="" /> {Parser.parse(`<@${x.id}>`, true, { channelId, viewingChannelId: channelId })} </div> ); })} { (Object.keys(RMC).length === 0) ? ( <div className={cl("member-list-footer")}> <ThreeDots dotRadius={5} themed={true} /> </div> ) : !RMC[roleId] ? ( <Text className={cl("member-list-footer")} variant="text-md/normal">No member found with this role</Text> ) : RMC[roleId] === roleMembers.length ? ( <> <div className={cl("divider")} /> <Text className={cl("member-list-footer")} variant="text-md/normal">All members loaded</Text> </> ) : rolesFetched.includes(roleId) ? ( <> <div className={cl("divider")} /> <Text className={cl("member-list-footer")} variant="text-md/normal">All cached members loaded</Text> </> ) : ( <div className={cl("member-list-footer")}> <ThreeDots dotRadius={5} themed={true} /> </div> ) } </ScrollerThin> </div> ); } 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]); let cooldown; useEffect(() => { const timeout = setTimeout(() => cooldown = false, 1000); return () => clearTimeout(timeout); }, [selectedRole]); return ( <ErrorBoundary> <ModalRoot {...props} size={ModalSize.LARGE}> <ModalHeader> <Text className={cl("modal-title")} variant="heading-lg/semibold">View members with role</Text> <ModalCloseButton onClick={props.onClose} /> </ModalHeader> <ModalContent className={cl("modal-content")}> <div className={cl("modal-container")}> <ScrollerThin className={cl("modal-list")} orientation="auto"> {roles.map((role, index) => { if (role.id === guildId) return; const roleIconSrc = role != null ? getRoleIconSrc(role) : undefined; return ( <div className={cl("modal-list-item-btn")} 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} > <div className={cl("modal-list-item", { "modal-list-item-active": selectedRole.id === role.id })} > <span className={cl("modal-role-circle")} style={{ backgroundColor: role?.colorString || "var(--primary-300)" }} /> { roleIconSrc != null && ( <img className={cl("modal-role-image")} src={roleIconSrc} /> ) } <Text variant="text-md/normal"> {role?.name || "Unknown role"} </Text> </div> </div> ); })} </ScrollerThin> <div className={cl("modal-divider")} /> <MembersContainer guildId={guildId} roleId={selectedRole.id} /> </div> </ModalContent> </ModalRoot > </ErrorBoundary> ); } export function showInRoleModal(guildId: string, roleId: string) { openModal(props => <InRoleModal guildId={guildId} props={props} roleId={roleId} /> ); }