Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New plugin: ViewMembersWithRole #3163

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/plugins/viewMembersWithRole/README.md
Original file line number Diff line number Diff line change
@@ -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

<img width="184" alt="image" src="https://github.com/user-attachments/assets/eefb2044-4f67-4acc-ab33-21fd07ed9a27" />

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.

213 changes: 213 additions & 0 deletions src/plugins/viewMembersWithRole/componenents/ViewMembersModal.tsx
Original file line number Diff line number Diff line change
@@ -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<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]);
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 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 (
<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={() => selectRole(roles[index])}
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 >
);
}

export function openVMWRModal(guildId) {

openModal(props =>
<VMWRModal
guildId={guildId}
props={props}
/>
);
}

17 changes: 17 additions & 0 deletions src/plugins/viewMembersWithRole/componenents/icons.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<svg
height="20"
width="20"
viewBox="0 0 24 24"
>
<path fill="currentColor" d="M14.5 8a3 3 0 1 0-2.7-4.3c-.2.4.06.86.44 1.12a5 5 0 0 1 2.14 3.08c.01.06.06.1.12.1ZM18.44 17.27c.15.43.54.73 1 .73h1.06c.83 0 1.5-.67 1.5-1.5a7.5 7.5 0 0 0-6.5-7.43c-.55-.08-.99.38-1.1.92-.06.3-.15.6-.26.87-.23.58-.05 1.3.47 1.63a9.53 9.53 0 0 1 3.83 4.78ZM12.5 9a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM2 20.5a7.5 7.5 0 0 1 15 0c0 .83-.67 1.5-1.5 1.5a.2.2 0 0 1-.2-.16c-.2-.96-.56-1.87-.88-2.54-.1-.23-.42-.15-.42.1v2.1a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2.1c0-.25-.31-.33-.42-.1-.32.67-.67 1.58-.88 2.54a.2.2 0 0 1-.2.16A1.5 1.5 0 0 1 2 20.5Z" />
</svg>
);
}
44 changes: 44 additions & 0 deletions src/plugins/viewMembersWithRole/index.tsx
Original file line number Diff line number Diff line change
@@ -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(
<Menu.MenuItem
label="View members with role"
id="vmwr-menuitem"
icon={MemberIcon}
action={() => 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() { },
});
Loading