Upload files to "/"

This commit is contained in:
Ryfter 2025-01-25 11:38:02 -05:00
parent cf0114bebe
commit 7e3241e4fc
3 changed files with 340 additions and 49 deletions

View file

@ -4,40 +4,210 @@
* 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, 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 =>
<>
<ErrorBoundary>
<ModalRoot {...props} size={ModalSize.DYNAMIC} fullscreenOnMobile={true} >
<ModalHeader className={cl("header")}>
<Forms.FormText style={{ fontSize: "1.2rem", fontWeight: "bold", marginRight: "7px" }}>Members of role {
Parser.parse(`<@&${roleId}>`, true, { channelId, viewingChannelId: channelId })
} ({members.length})</Forms.FormText>
<ModalCloseButton onClick={props.onClose} className={cl("close")} />
</ModalHeader>
<ModalContent>
<div style={{ padding: "13px 20px" }} className={cl("member-list")}>
{
members.length !== 0 ? members.map(member =>
<>
<Forms.FormText className={cl("modal-member")}>
{Parser.parse(`<@${member.userId}>`, true, { channelId, viewingChannelId: channelId })}
</Forms.FormText>
</>
) : <Forms.FormText>Looks like no online cached members with that role were found. Try scrolling down on your member list to cache more users!</Forms.FormText>
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 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]);
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 >
</ErrorBoundary>
</>
);
}
export function showInRoleModal(guildId: string, roleId: string) {
openModal(props =>
<InRoleModal
guildId={guildId}
props={props}
roleId={roleId}
/>
);
}

144
style.css
View file

@ -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);
}

11
utils.ts Normal file
View file

@ -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");