diff options
-rw-r--r-- | src/plugins/permissionsViewer/index.tsx | 4 | ||||
-rw-r--r-- | src/plugins/serverProfile/GuildProfileModal.tsx | 247 | ||||
-rw-r--r-- | src/plugins/serverProfile/README.md | 7 | ||||
-rw-r--r-- | src/plugins/serverProfile/index.tsx | 40 | ||||
-rw-r--r-- | src/plugins/serverProfile/styles.css | 97 |
5 files changed, 393 insertions, 2 deletions
diff --git a/src/plugins/permissionsViewer/index.tsx b/src/plugins/permissionsViewer/index.tsx index 480efc1..7de29b3 100644 --- a/src/plugins/permissionsViewer/index.tsx +++ b/src/plugins/permissionsViewer/index.tsx @@ -178,12 +178,12 @@ export default definePlugin({ start() { addContextMenuPatch("user-context", this.userContextMenuPatch); addContextMenuPatch("channel-context", this.channelContextMenuPatch); - addContextMenuPatch("guild-context", this.guildContextMenuPatch); + addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch); }, stop() { removeContextMenuPatch("user-context", this.userContextMenuPatch); removeContextMenuPatch("channel-context", this.channelContextMenuPatch); - removeContextMenuPatch("guild-context", this.guildContextMenuPatch); + removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch); }, }); 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> + ); +} diff --git a/src/plugins/serverProfile/README.md b/src/plugins/serverProfile/README.md new file mode 100644 index 0000000..9da70e7 --- /dev/null +++ b/src/plugins/serverProfile/README.md @@ -0,0 +1,7 @@ +# ServerProfile + +Allows you to view info about servers and see friends and blocked users + +![image](https://github.com/Vendicated/Vencord/assets/45497981/a49783b5-e8fc-41d8-968f-58600e9f6580) +![image](https://github.com/Vendicated/Vencord/assets/45497981/5efc158a-e671-4196-a15a-77edf79a2630) +![image](https://github.com/Vendicated/Vencord/assets/45497981/f43be943-6dc4-4232-9709-fbeb382d8e54) diff --git a/src/plugins/serverProfile/index.tsx b/src/plugins/serverProfile/index.tsx new file mode 100644 index 0000000..c27f8cd --- /dev/null +++ b/src/plugins/serverProfile/index.tsx @@ -0,0 +1,40 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { Menu } from "@webpack/common"; +import { Guild } from "discord-types/general"; + +import { openGuildProfileModal } from "./GuildProfileModal"; + +const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => () => { + const group = findGroupChildrenByChildId("privacy", children); + + group?.push( + <Menu.MenuItem + id="vc-server-profile" + label="Server Profile" + action={() => openGuildProfileModal(guild)} + /> + ); +}; + +export default definePlugin({ + name: "ServerProfile", + description: "Allows you to view info about a server by right clicking it in the server list", + authors: [Devs.Ven, Devs.Nuckyz], + tags: ["guild", "info"], + + start() { + addContextMenuPatch(["guild-context", "guild-header-popout"], Patch); + }, + + stop() { + removeContextMenuPatch(["guild-context", "guild-header-popout"], Patch); + } +}); diff --git a/src/plugins/serverProfile/styles.css b/src/plugins/serverProfile/styles.css new file mode 100644 index 0000000..87487ec --- /dev/null +++ b/src/plugins/serverProfile/styles.css @@ -0,0 +1,97 @@ +.vc-gp-root { + height: 100%; + user-select: text; +} + +.vc-gp-banner { + width: 100%; + cursor: pointer; +} + +.vc-gp-header { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5em; + margin: 0.5em; +} + +.vc-gp-header img { + width: 48px; + height: 48px; + cursor: pointer; +} + +.vc-gp-name-and-description { + display: flex; + flex-direction: column; + gap: 0.2em; +} + +.vc-gp-name { + margin: 0; +} + +.vc-gp-tab-bar { + border-bottom: 2px solid var(--background-modifier-accent); + margin: 20px 12px 0; + display: flex; + gap: 40px; + align-items: stretch; + flex-direction: row; +} + +.vc-gp-tab { + border-bottom: 2px solid transparent; + color: var(--interactive-normal); + cursor: pointer; + height: 39px; + line-height: 14px; +} + +.vc-gp-tab-content { + margin: 1em; +} + +.vc-gp-tab:where(.vc-gp-selected, :hover, :focus) { + border-bottom-color: var(--interactive-active); +} + +.vc-gp-info { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1em; +} + +.vc-gp-server-info-pair { + color: var(--text-normal); +} + +.vc-gp-server-info-pair [class^="timestamp"] { + margin-left: 0; +} + +.vc-gp-owner { + display: flex; + align-items: center; + gap: 0.2em; +} + +.vc-gp-owner img { + height: 20px; + border-radius: 50%; + cursor: pointer; +} + +.vc-gp-scroller { + width: 100%; + max-height: 500px; +} + +.vc-gp-scroller [class^="listRow"] { + margin: 1px 0; +} + +.vc-gp-scroller [class^="listRow"]:hover { + background-color: var(--background-modifier-hover); +} |