diff --git a/RoleMembersModal.tsx b/RoleMembersModal.tsx index 3ef6cb4..76c5756 100644 --- a/RoleMembersModal.tsx +++ b/RoleMembersModal.tsx @@ -4,40 +4,225 @@ * 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, showToast, 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"); + +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); + 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 ( +
+
+
+ + {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]); + + let cooldown; + useEffect(() => { + const timeout = setTimeout(() => cooldown = false, 1000); + return () => clearTimeout(timeout); + }, [selectedRole]); + + return ( + + + + View members with role + + + +
+ + {roles.map((role, index) => { + + if (role.id === guildId) return; + + const roleIconSrc = role != null ? getRoleIconSrc(role) : undefined; + + return ( +
{ + 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} + > +
+ + { + roleIconSrc != null && ( + + ) + + } + + {role?.name || "Unknown role"} + +
+
+ ); + })} +
+
+ +
+ + + + ); +} + +export function showInRoleModal(guildId: string, roleId: string) { + openModal(props => + + ); +} + 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..7d858d5 100644 --- a/index.tsx +++ b/index.tsx @@ -7,33 +7,29 @@ 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")!; -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!)", - 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 +40,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 +65,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 +85,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")} /> ); } 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");