diff --git a/src/components/PluginSettings/ContributorModal.tsx b/src/components/PluginSettings/ContributorModal.tsx new file mode 100644 index 000000000..82c230259 --- /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 => + + + + + + + + ); +} + +function GithubIcon() { + const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark; + return GitHub; +} + +function WebsiteIcon() { + const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark; + return 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 ( + <> +
+ + {user.username} + +
+ {website && ( + + + + )} + {githubName && ( + + + + )} +
+
+ +
+ {plugins.map(p => + showToast("Restart to apply changes!")} + /> + )} +
+ + ); +} diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index f30cedeef..78f3c9db7 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) => ( + openContributorModal(user)} + > + {user.username} + + )} /> diff --git a/src/components/PluginSettings/contributorModal.css b/src/components/PluginSettings/contributorModal.css new file mode 100644 index 000000000..a8af8c8b8 --- /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 f19d32647..3712f3de1 100644 --- a/src/components/PluginSettings/index.tsx +++ b/src/components/PluginSettings/index.tsx @@ -91,7 +91,7 @@ interface PluginCardProps extends React.HTMLProps { 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 5e33e4b32..000000000 --- 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 92f8c2578..f2dee11d9 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 8bde10e9a..eac204b7d 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 404a8db1b..1f6ef34ef 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 4f4326b65..458509b42 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 456255d30..d42cb6b56 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; export let ChannelStore: Stores.ChannelStore & t.FluxStore; @@ -86,6 +87,7 @@ export const useStateFromStores: ( 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 4b316aad6..5d5424fe2 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; + rel?: string; + target?: string; + title?: string, + className?: string; + tabIndex?: number; + onClick?(): void; + trusted?: boolean; + messageId?: string; + channelId?: string; +}>>; export type ScrollerThin = ComponentType