aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/PluginSettings/ContributorModal.tsx113
-rw-r--r--src/components/PluginSettings/PluginModal.tsx29
-rw-r--r--src/components/PluginSettings/contributorModal.css57
-rw-r--r--src/components/PluginSettings/index.tsx2
-rw-r--r--src/components/PluginSettings/userPopoutHideBotTag.css3
-rw-r--r--src/components/VencordSettings/addonCard.css1
-rw-r--r--src/plugins/pronoundb/pronoundbUtils.ts5
-rw-r--r--src/plugins/showConnections/index.tsx3
-rw-r--r--src/utils/discord.tsx37
-rw-r--r--src/webpack/common/stores.ts2
-rw-r--r--src/webpack/common/types/components.d.ts16
11 files changed, 238 insertions, 30 deletions
diff --git a/src/components/PluginSettings/ContributorModal.tsx b/src/components/PluginSettings/ContributorModal.tsx
new file mode 100644
index 0000000..82c2302
--- /dev/null
+++ b/src/components/PluginSettings/ContributorModal.tsx
@@ -0,0 +1,113 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2023 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import "./contributorModal.css";
+
+import { useSettings } from "@api/Settings";
+import { classNameFactory } from "@api/Styles";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { DevsById } from "@utils/constants";
+import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
+import { ModalContent, ModalRoot, openModal } from "@utils/modal";
+import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
+import { User } from "discord-types/general";
+
+import Plugins from "~plugins";
+
+import { PluginCard } from ".";
+
+const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
+const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
+const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
+const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
+
+const cl = classNameFactory("vc-author-modal-");
+
+export function openContributorModal(user: User) {
+ openModal(modalProps =>
+ <ModalRoot {...modalProps}>
+ <ErrorBoundary>
+ <ModalContent className={cl("root")}>
+ <ContributorModal user={user} />
+ </ModalContent>
+ </ErrorBoundary>
+ </ModalRoot>
+ );
+}
+
+function GithubIcon() {
+ const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
+ return <img src={src} alt="GitHub" />;
+}
+
+function WebsiteIcon() {
+ const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
+ return <img src={src} alt="Website" />;
+}
+
+function ContributorModal({ user }: { user: User; }) {
+ useSettings();
+
+ const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id));
+
+ useEffect(() => {
+ if (!profile && !user.bot && user.id)
+ fetchUserProfile(user.id);
+ }, [user.id]);
+
+ const githubName = profile?.connectedAccounts?.find(a => a.type === "github")?.name;
+ const website = profile?.connectedAccounts?.find(a => a.type === "domain")?.name;
+
+ const plugins = useMemo(() => {
+ const allPlugins = Object.values(Plugins);
+ const pluginsByAuthor = DevsById[user.id]
+ ? allPlugins.filter(p => p.authors.includes(DevsById[user.id]))
+ : allPlugins.filter(p => p.authors.some(a => a.name === user.username));
+
+ return pluginsByAuthor
+ .filter(p => !p.name.endsWith("API"))
+ .sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
+ }, [user.id, user.username]);
+
+ return (
+ <>
+ <div className={cl("header")}>
+ <img
+ className={cl("avatar")}
+ src={user.getAvatarURL(void 0, 512, true)}
+ alt=""
+ />
+ <Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle>
+
+ <div className={cl("links")}>
+ {website && (
+ <MaskedLink
+ href={"https://" + website}
+ >
+ <WebsiteIcon />
+ </MaskedLink>
+ )}
+ {githubName && (
+ <MaskedLink href={`https://github.com/${githubName}`}>
+ <GithubIcon />
+ </MaskedLink>
+ )}
+ </div>
+ </div>
+
+ <div className={cl("plugins")}>
+ {plugins.map(p =>
+ <PluginCard
+ key={p.name}
+ plugin={p}
+ disabled={p.required ?? false}
+ onRestartNeeded={() => showToast("Restart to apply changes!")}
+ />
+ )}
+ </div>
+ </>
+ );
+}
diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx
index f30cede..78f3c9d 100644
--- a/src/components/PluginSettings/PluginModal.tsx
+++ b/src/components/PluginSettings/PluginModal.tsx
@@ -18,7 +18,6 @@
import { generateId } from "@api/Commands";
import { useSettings } from "@api/Settings";
-import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { proxyLazy } from "@utils/lazy";
@@ -28,7 +27,7 @@ import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, M
import { LazyComponent } from "@utils/react";
import { OptionType, Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack";
-import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
+import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
import { User } from "discord-types/general";
import { Constructor } from "type-fest";
@@ -41,7 +40,7 @@ import {
SettingSliderComponent,
SettingTextComponent
} from "./components";
-import hideBotTagStyle from "./userPopoutHideBotTag.css?managed";
+import { openContributorModal } from "./ContributorModal";
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
@@ -92,27 +91,16 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
React.useEffect(() => {
- enableStyle(hideBotTagStyle);
-
- let originalUser: User;
(async () => {
for (const user of plugin.authors.slice(0, 6)) {
const author = user.id
? await UserUtils.fetchUser(`${user.id}`)
- // only show name & pfp and no actions so users cannot harass plugin devs for support (send dms, add as friend, etc)
- .then(u => (originalUser = u, makeDummyUser(u)))
.catch(() => makeDummyUser({ username: user.name }))
: makeDummyUser({ username: user.name });
setAuthors(a => [...a, author]);
}
})();
-
- return () => {
- disableStyle(hideBotTagStyle);
- if (originalUser)
- FluxDispatcher.dispatch({ type: "USER_UPDATE", user: originalUser });
- };
}, []);
async function saveAndClose() {
@@ -214,6 +202,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
showDefaultAvatarsForNullUsers
showUserPopout
renderMoreUsers={renderMoreUsers}
+ renderUser={(user: User) => (
+ <Clickable
+ className={AvatarStyles.clickableAvatar}
+ onClick={() => openContributorModal(user)}
+ >
+ <img
+ className={AvatarStyles.avatar}
+ src={user.getAvatarURL(void 0, 80, true)}
+ alt={user.username}
+ title={user.username}
+ />
+ </Clickable>
+ )}
/>
</div>
</Forms.FormSection>
diff --git a/src/components/PluginSettings/contributorModal.css b/src/components/PluginSettings/contributorModal.css
new file mode 100644
index 0000000..a8af8c8
--- /dev/null
+++ b/src/components/PluginSettings/contributorModal.css
@@ -0,0 +1,57 @@
+.vc-author-modal-root {
+ padding: 1em;
+}
+
+.vc-author-modal-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 1em;
+}
+
+.vc-author-modal-name {
+ text-transform: none;
+ flex-grow: 0;
+ background: var(--background-tertiary);
+ border-radius: 0 9999px 9999px 0;
+ padding: 6px 0.8em 6px 0.5em;
+ font-size: 20px;
+ height: 20px;
+ position: relative;
+}
+
+.vc-author-modal-name::before {
+ content: "";
+ display: block;
+ position: absolute;
+ height: 100%;
+ width: 16px;
+ background: var(--background-tertiary);
+ z-index: -1;
+ left: -16px;
+ top: 0;
+}
+
+.vc-author-modal-avatar {
+ height: 32px;
+ width: 32px;
+ border-radius: 50%;
+}
+
+.vc-author-modal-links {
+ margin-left: auto;
+ display: flex;
+ gap: 0.2em;
+}
+
+.vc-author-modal-links img {
+ height: 32px;
+ width: 32px;
+ border-radius: 50%;
+ border: 4px solid var(--background-tertiary);
+ box-sizing: border-box
+}
+
+.vc-author-modal-plugins {
+ display: grid;
+ gap: 0.5em;
+}
diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx
index f19d326..3712f3d 100644
--- a/src/components/PluginSettings/index.tsx
+++ b/src/components/PluginSettings/index.tsx
@@ -91,7 +91,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
isNew?: boolean;
}
-function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
+export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
const settings = Settings.plugins[plugin.name];
const isEnabled = () => settings.enabled ?? false;
diff --git a/src/components/PluginSettings/userPopoutHideBotTag.css b/src/components/PluginSettings/userPopoutHideBotTag.css
deleted file mode 100644
index 5e33e4b..0000000
--- a/src/components/PluginSettings/userPopoutHideBotTag.css
+++ /dev/null
@@ -1,3 +0,0 @@
-[class|="userPopoutOuter"] [class*="botTag"] {
- display: none;
-}
diff --git a/src/components/VencordSettings/addonCard.css b/src/components/VencordSettings/addonCard.css
index 92f8c25..f2dee11 100644
--- a/src/components/VencordSettings/addonCard.css
+++ b/src/components/VencordSettings/addonCard.css
@@ -8,6 +8,7 @@
width: 100%;
transition: 0.1s ease-out;
transition-property: box-shadow, transform, background, opacity;
+ box-sizing: border-box;
}
.vc-addon-card-disabled {
diff --git a/src/plugins/pronoundb/pronoundbUtils.ts b/src/plugins/pronoundb/pronoundbUtils.ts
index 8bde10e..eac204b 100644
--- a/src/plugins/pronoundb/pronoundbUtils.ts
+++ b/src/plugins/pronoundb/pronoundbUtils.ts
@@ -21,14 +21,11 @@ import { VENCORD_USER_AGENT } from "@utils/constants";
import { debounce } from "@utils/debounce";
import { getCurrentChannel } from "@utils/discord";
import { useAwaiter } from "@utils/react";
-import { findStoreLazy } from "@webpack";
-import { UserStore } from "@webpack/common";
+import { UserProfileStore, UserStore } from "@webpack/common";
import { settings } from "./settings";
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
-const UserProfileStore = findStoreLazy("UserProfileStore");
-
type PronounsWithSource = [string | null, string];
const EmptyPronouns: PronounsWithSource = [null, ""];
diff --git a/src/plugins/showConnections/index.tsx b/src/plugins/showConnections/index.tsx
index 404a8db..1f6ef34 100644
--- a/src/plugins/showConnections/index.tsx
+++ b/src/plugins/showConnections/index.tsx
@@ -27,13 +27,12 @@ import { copyWithToast } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCode, findByCodeLazy, findByPropsLazy, findStoreLazy } from "@webpack";
-import { Text, Tooltip } from "@webpack/common";
+import { Text, Tooltip, UserProfileStore } from "@webpack/common";
import { User } from "discord-types/general";
import { VerifiedIcon } from "./VerifiedIcon";
const Section = LazyComponent(() => findByCode("().lastSection"));
-const UserProfileStore = findStoreLazy("UserProfileStore");
const ThemeStore = findStoreLazy("ThemeStore");
const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl");
const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"');
diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx
index 4f4326b..458509b 100644
--- a/src/utils/discord.tsx
+++ b/src/utils/discord.tsx
@@ -18,7 +18,7 @@
import { MessageObject } from "@api/MessageEvents";
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
-import { ChannelStore, ComponentDispatch, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, SelectedChannelStore, SelectedGuildStore, UserUtils } from "@webpack/common";
+import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileStore, UserUtils } from "@webpack/common";
import { Guild, Message, User } from "discord-types/general";
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
@@ -118,6 +118,41 @@ export async function openUserProfile(id: string) {
});
}
+interface FetchUserProfileOptions {
+ friend_token?: string;
+ connections_role_id?: string;
+ guild_id?: string;
+ with_mutual_guilds?: boolean;
+ with_mutual_friends_count?: boolean;
+}
+
+/**
+ * Fetch a user's profile
+ */
+export async function fetchUserProfile(id: string, options?: FetchUserProfileOptions) {
+ const cached = UserProfileStore.getUserProfile(id);
+ if (cached) return cached;
+
+ FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_START", userId: id });
+
+ const { body } = await RestAPI.get({
+ url: `/users/${id}/profile`,
+ query: {
+ with_mutual_guilds: false,
+ with_mutual_friends_count: false,
+ ...options
+ },
+ oldFormErrors: true,
+ });
+
+ FluxDispatcher.dispatch({ type: "USER_UPDATE", user: body.user });
+ await FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_SUCCESS", ...body });
+ if (options?.guild_id && body.guild_member)
+ FluxDispatcher.dispatch({ type: "GUILD_MEMBER_PROFILE_UPDATE", guildId: options.guild_id, guildMember: body.guild_member });
+
+ return UserProfileStore.getUserProfile(id);
+}
+
/**
* Get the unique username for a user. Returns user.username for pomelo people, user.tag otherwise
*/
diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts
index 456255d..d42cb6b 100644
--- a/src/webpack/common/stores.ts
+++ b/src/webpack/common/stores.ts
@@ -48,6 +48,7 @@ export let PoggerModeSettingsStore: GenericStore;
export let GuildStore: Stores.GuildStore & t.FluxStore;
export let UserStore: Stores.UserStore & t.FluxStore;
+export let UserProfileStore: GenericStore;
export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore;
export let SelectedGuildStore: t.FluxStore & Record<string, any>;
export let ChannelStore: Stores.ChannelStore & t.FluxStore;
@@ -86,6 +87,7 @@ export const useStateFromStores: <T>(
waitForStore("DraftStore", s => DraftStore = s);
waitForStore("UserStore", s => UserStore = s);
+waitForStore("UserProfileStore", m => UserProfileStore = m);
waitForStore("ChannelStore", m => ChannelStore = m);
waitForStore("SelectedChannelStore", m => SelectedChannelStore = m);
waitForStore("SelectedGuildStore", m => SelectedGuildStore = m);
diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts
index 4b316aa..5d5424f 100644
--- a/src/webpack/common/types/components.d.ts
+++ b/src/webpack/common/types/components.d.ts
@@ -398,12 +398,18 @@ export type Paginator = ComponentType<{
hideMaxPage?: boolean;
}>;
-export type MaskedLink = ComponentType<{
- onClick(): void;
- trusted: boolean;
- title: string,
+export type MaskedLink = ComponentType<PropsWithChildren<{
href: string;
-}>;
+ rel?: string;
+ target?: string;
+ title?: string,
+ className?: string;
+ tabIndex?: number;
+ onClick?(): void;
+ trusted?: boolean;
+ messageId?: string;
+ channelId?: string;
+}>>;
export type ScrollerThin = ComponentType<PropsWithChildren<{
className?: string;