new plugin: ServerProfile (#1704)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
This commit is contained in:
parent
c165725297
commit
2c758ccdf8
5 changed files with 393 additions and 2 deletions
|
@ -178,12 +178,12 @@ export default definePlugin({
|
||||||
start() {
|
start() {
|
||||||
addContextMenuPatch("user-context", this.userContextMenuPatch);
|
addContextMenuPatch("user-context", this.userContextMenuPatch);
|
||||||
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
||||||
addContextMenuPatch("guild-context", this.guildContextMenuPatch);
|
addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
removeContextMenuPatch("user-context", this.userContextMenuPatch);
|
removeContextMenuPatch("user-context", this.userContextMenuPatch);
|
||||||
removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
||||||
removeContextMenuPatch("guild-context", this.guildContextMenuPatch);
|
removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
247
src/plugins/serverProfile/GuildProfileModal.tsx
Normal file
247
src/plugins/serverProfile/GuildProfileModal.tsx
Normal file
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
7
src/plugins/serverProfile/README.md
Normal file
7
src/plugins/serverProfile/README.md
Normal file
|
@ -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)
|
40
src/plugins/serverProfile/index.tsx
Normal file
40
src/plugins/serverProfile/index.tsx
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
97
src/plugins/serverProfile/styles.css
Normal file
97
src/plugins/serverProfile/styles.css
Normal file
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in a new issue