diff options
author | V <vendicated@riseup.net> | 2023-09-05 18:34:12 +0200 |
---|---|---|
committer | V <vendicated@riseup.net> | 2023-09-05 18:36:14 +0200 |
commit | 2c758ccdf8372c48f2d2a79ece8f2eba63f1510c (patch) | |
tree | dfd63397b054a2c3ab48550109dfc75a4c7b38f3 /src/plugins/serverProfile/GuildProfileModal.tsx | |
parent | c165725297a9e8aadea15cb22957db38b37c4515 (diff) | |
download | Vencord-2c758ccdf8372c48f2d2a79ece8f2eba63f1510c.tar.gz Vencord-2c758ccdf8372c48f2d2a79ece8f2eba63f1510c.tar.bz2 Vencord-2c758ccdf8372c48f2d2a79ece8f2eba63f1510c.zip |
new plugin: ServerProfile (#1704)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Diffstat (limited to 'src/plugins/serverProfile/GuildProfileModal.tsx')
-rw-r--r-- | src/plugins/serverProfile/GuildProfileModal.tsx | 247 |
1 files changed, 247 insertions, 0 deletions
diff --git a/src/plugins/serverProfile/GuildProfileModal.tsx b/src/plugins/serverProfile/GuildProfileModal.tsx new file mode 100644 index 0000000..79b3877 --- /dev/null +++ b/src/plugins/serverProfile/GuildProfileModal.tsx @@ -0,0 +1,247 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { classNameFactory } from "@api/Styles"; +import { openImageModal, openUserProfile } from "@utils/discord"; +import { classes } from "@utils/misc"; +import { ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { LazyComponent, useAwaiter } from "@utils/react"; +import { findByCode, findByPropsLazy } from "@webpack"; +import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, moment, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common"; +import { Guild, User } from "discord-types/general"; + +const IconUtils = findByPropsLazy("getGuildBannerURL"); +const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper"); +const UserRow = LazyComponent(() => findByCode(".listDiscriminator")); + +const cl = classNameFactory("vc-gp-"); + +export function openGuildProfileModal(guild: Guild) { + openModal(props => + <ModalRoot {...props} size={ModalSize.MEDIUM}> + <GuildProfileModal guild={guild} /> + </ModalRoot> + ); +} + +const enum Tabs { + ServerInfo, + Friends, + BlockedUsers +} + +interface GuildProps { + guild: Guild; +} + +interface RelationshipProps extends GuildProps { + setCount(count: number): void; +} + +const fetched = { + friends: false, + blocked: false +}; + +function renderTimestamp(timestamp: number) { + return ( + <Timestamp timestamp={moment(timestamp)} /> + ); +} + +function GuildProfileModal({ guild }: GuildProps) { + const [friendCount, setFriendCount] = useState<number>(); + const [blockedCount, setBlockedCount] = useState<number>(); + + useEffect(() => { + fetched.friends = false; + fetched.blocked = false; + }, []); + + const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo); + + const bannerUrl = guild.banner && IconUtils.getGuildBannerURL({ + id: guild.id, + banner: guild.banner + }, true).replace(/\?size=\d+$/, "?size=1024"); + + const iconUrl = guild.icon && IconUtils.getGuildIconURL({ + id: guild.id, + icon: guild.icon, + canAnimate: true, + size: 512 + }); + + return ( + <div className={cl("root")}> + {bannerUrl && currentTab === Tabs.ServerInfo && ( + <img + className={cl("banner")} + src={bannerUrl} + alt="" + onClick={() => openImageModal(bannerUrl)} + /> + )} + + <div className={cl("header")}> + {guild.icon + ? <img + src={iconUrl} + alt="" + onClick={() => openImageModal(iconUrl)} + /> + : <div aria-hidden className={classes(IconClasses.childWrapper, IconClasses.acronym)}>{guild.acronym}</div> + } + + <div className={cl("name-and-description")}> + <Forms.FormTitle tag="h5" className={cl("name")}>{guild.name}</Forms.FormTitle> + {guild.description && <Forms.FormText>{guild.description}</Forms.FormText>} + </div> + </div> + + <TabBar + type="top" + look="brand" + className={cl("tab-bar")} + selectedItem={currentTab} + onItemSelect={setCurrentTab} + > + <TabBar.Item + className={cl("tab", { selected: currentTab === Tabs.ServerInfo })} + id={Tabs.ServerInfo} + > + Server Info + </TabBar.Item> + <TabBar.Item + className={cl("tab", { selected: currentTab === Tabs.Friends })} + id={Tabs.Friends} + > + Friends{friendCount !== undefined ? ` (${friendCount})` : ""} + </TabBar.Item> + <TabBar.Item + className={cl("tab", { selected: currentTab === Tabs.BlockedUsers })} + id={Tabs.BlockedUsers} + > + Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""} + </TabBar.Item> + </TabBar> + + <div className={cl("tab-content")}> + {currentTab === Tabs.ServerInfo && <ServerInfoTab guild={guild} />} + {currentTab === Tabs.Friends && <FriendsTab guild={guild} setCount={setFriendCount} />} + {currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />} + </div> + </div> + ); +} + + +function Owner(guildId: string, owner: User) { + const guildAvatar = GuildMemberStore.getMember(guildId, owner.id)?.avatar; + const ownerAvatarUrl = + guildAvatar + ? IconUtils.getGuildMemberAvatarURLSimple({ + userId: owner!.id, + avatar: guildAvatar, + guildId, + canAnimate: true + }, true) + : IconUtils.getUserAvatarURL(owner, true); + + return ( + <div className={cl("owner")}> + <img src={ownerAvatarUrl} alt="" onClick={() => openImageModal(ownerAvatarUrl)} /> + {Parser.parse(`<@${owner.id}>`)} + </div> + ); +} + +function ServerInfoTab({ guild }: GuildProps) { + const [owner] = useAwaiter(() => UserUtils.fetchUser(guild.ownerId), { + deps: [guild.ownerId], + fallbackValue: null + }); + + const Fields = { + "Server Owner": owner ? Owner(guild.id, owner) : "Loading...", + "Created At": renderTimestamp(SnowflakeUtils.extractTimestamp(guild.id)), + "Joined At": renderTimestamp(guild.joinedAt.getTime()), + "Vanity Link": guild.vanityURLCode ? (<a>{`discord.gg/${guild.vanityURLCode}`}</a>) : "-", // Making the anchor href valid would cause Discord to reload + "Preferred Locale": guild.preferredLocale || "-", + "Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?", + "Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`, + "Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category + "Roles": Object.keys(guild.roles).length - 1, // - @everyone + }; + + return ( + <div className={cl("info")}> + {Object.entries(Fields).map(([name, node]) => + <div className={cl("server-info-pair")} key={name}> + <Forms.FormTitle tag="h5">{name}</Forms.FormTitle> + {typeof node === "string" ? <span>{node}</span> : node} + </div> + )} + </div> + ); +} + +function FriendsTab({ guild, setCount }: RelationshipProps) { + return UserList("friends", guild, RelationshipStore.getFriendIDs(), setCount); +} + +function BlockedUsersTab({ guild, setCount }: RelationshipProps) { + const blockedIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isBlocked(id)); + return UserList("blocked", guild, blockedIds, setCount); +} + +function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setCount: (count: number) => void) { + const missing = [] as string[]; + const members = [] as string[]; + + for (const id of ids) { + if (GuildMemberStore.isMember(guild.id, id)) + members.push(id); + else + missing.push(id); + } + + // Used for side effects (rerender on member request success) + useStateFromStores( + [GuildMemberStore], + () => GuildMemberStore.getMemberIds(guild.id), + null, + (old, curr) => old.length === curr.length + ); + + useEffect(() => { + if (!fetched[type] && missing.length) { + fetched[type] = true; + FluxDispatcher.dispatch({ + type: "GUILD_MEMBERS_REQUEST", + guildIds: [guild.id], + userIds: missing + }); + } + }, []); + + useEffect(() => setCount(members.length), [members.length]); + + return ( + <ScrollerThin fade className={cl("scroller")}> + {members.map(id => + <UserRow + user={UserStore.getUser(id)} + status={PresenceStore.getStatus(id) || "offline"} + onSelect={() => openUserProfile(id)} + onContextMenu={() => { }} + /> + )} + </ScrollerThin> + ); +} |