forked from userplugins/in-role
Ryfter
56d5b10e1b
added a 1s cooldown between each role selection to stop ratelimits, except if the role has already been fetched
228 lines
10 KiB
TypeScript
228 lines
10 KiB
TypeScript
/*
|
|
* 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}
|
|
/>
|
|
);
|
|
}
|
|
|