aboutsummaryrefslogtreecommitdiff
path: root/src/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/permissionsViewer/index.tsx4
-rw-r--r--src/plugins/serverProfile/GuildProfileModal.tsx247
-rw-r--r--src/plugins/serverProfile/README.md7
-rw-r--r--src/plugins/serverProfile/index.tsx40
-rw-r--r--src/plugins/serverProfile/styles.css97
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);
+}