From 6787e98003e963e9bccfa8f5386fb8c09d230a3a Mon Sep 17 00:00:00 2001 From: sadan4 <117494111+sadan4@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:38:27 -0400 Subject: [PATCH] FakeProfileThemes: fix error when own profile is not loaded (#3514) Co-authored-by: V --- packages/discord-types/enums/commands.ts | 5 + .../discord-types/src/common/Application.d.ts | 23 ++ packages/discord-types/src/common/User.d.ts | 3 +- packages/discord-types/src/common/index.d.ts | 1 + .../src/stores/UserProfileStore.d.ts | 151 +++++++++++++ packages/discord-types/src/stores/index.d.ts | 1 + src/plugins/fakeProfileThemes/index.tsx | 202 +++++++++--------- src/plugins/showConnections/index.tsx | 15 +- src/plugins/validUser/index.tsx | 18 +- src/utils/discord.tsx | 2 +- src/webpack/common/stores.ts | 2 +- 11 files changed, 297 insertions(+), 126 deletions(-) create mode 100644 packages/discord-types/src/common/Application.d.ts create mode 100644 packages/discord-types/src/stores/UserProfileStore.d.ts diff --git a/packages/discord-types/enums/commands.ts b/packages/discord-types/enums/commands.ts index 0556e72d..298f9b7a 100644 --- a/packages/discord-types/enums/commands.ts +++ b/packages/discord-types/enums/commands.ts @@ -25,3 +25,8 @@ export const enum ApplicationCommandType { USER = 2, MESSAGE = 3, } + +export const enum ApplicationIntegrationType { + GUILD_INSTALL = 0, + USER_INSTALL = 1 +} diff --git a/packages/discord-types/src/common/Application.d.ts b/packages/discord-types/src/common/Application.d.ts new file mode 100644 index 00000000..d2ec1e7e --- /dev/null +++ b/packages/discord-types/src/common/Application.d.ts @@ -0,0 +1,23 @@ +import { User } from "./User"; + +export interface Application { + id: string; + name: string; + description?: string | null; + type: number | null; + icon: string | null | undefined; + is_discoverable: boolean; + is_monetized: boolean; + is_verified: boolean; + bot?: User; + deeplink_uri?: string; + flags?: number; + privacy_policy_url?: string; + terms_of_service_url?: string; + install_params?: ApplicationInstallParams; +} + +export interface ApplicationInstallParams { + permissions: string | null; + scopes: string[]; +} diff --git a/packages/discord-types/src/common/User.d.ts b/packages/discord-types/src/common/User.d.ts index a5503bcb..07eb4718 100644 --- a/packages/discord-types/src/common/User.d.ts +++ b/packages/discord-types/src/common/User.d.ts @@ -6,7 +6,7 @@ export class User extends DiscordRecord { constructor(user: object); accentColor: number; avatar: string; - banner: string; + banner: string | null | undefined; bio: string; bot: boolean; desktop: boolean; @@ -27,7 +27,6 @@ export class User extends DiscordRecord { system: boolean; username: string; verified: boolean; - themeColors?: [number, number]; get createdAt(): Date; get hasPremiumPerks(): boolean; diff --git a/packages/discord-types/src/common/index.d.ts b/packages/discord-types/src/common/index.d.ts index f85a7c72..5e50e96e 100644 --- a/packages/discord-types/src/common/index.d.ts +++ b/packages/discord-types/src/common/index.d.ts @@ -1,3 +1,4 @@ +export * from "./Application"; export * from "./Channel"; export * from "./Guild"; export * from "./GuildMember"; diff --git a/packages/discord-types/src/stores/UserProfileStore.d.ts b/packages/discord-types/src/stores/UserProfileStore.d.ts new file mode 100644 index 00000000..d9efc47b --- /dev/null +++ b/packages/discord-types/src/stores/UserProfileStore.d.ts @@ -0,0 +1,151 @@ +import { FluxStore, Guild, User, Application, ApplicationInstallParams } from ".."; +import { ApplicationIntegrationType } from "../../enums"; + +export interface MutualFriend { + /** + * the userid of the mutual friend + */ + key: string; + /** + * the status of the mutual friend + */ + status: "online" | "offline" | "idle" | "dnd"; + /** + * the user object of the mutual friend + */ + user: User; +} + +export interface MutualGuild { + /** + * the guild object of the mutual guild + */ + guild: Guild; + /** + * the user's nickname in the guild, if any + */ + nick: string | null; + +} + +export interface ProfileBadge { + id: string; + description: string; + icon: string; + link?: string; +} + +export interface ConnectedAccount { + type: "twitch" | "youtube" | "skype" | "steam" | "leagueoflegends" | "battlenet" | "bluesky" | "bungie" | "reddit" | "twitter" | "twitter_legacy" | "spotify" | "facebook" | "xbox" | "samsung" | "contacts" | "instagram" | "mastodon" | "soundcloud" | "github" | "playstation" | "playstation-stg" | "epicgames" | "riotgames" | "roblox" | "paypal" | "ebay" | "tiktok" | "crunchyroll" | "domain" | "amazon-music"; + /** + * underlying id of connected account + * eg. account uuid + */ + id: string; + /** + * display name of connected account + */ + name: string; + verified: boolean; + metadata?: Record; +} + +export interface ProfileApplication { + id: string; + customInstallUrl: string | undefined; + installParams: ApplicationInstallParams | undefined; + flags: number; + popularApplicationCommandIds?: string[]; + integrationTypesConfig: Record>; + primarySkuId: string | undefined; + storefront_available: boolean; +} + +export interface UserProfileBase extends Pick { + accentColor: number | null; + /** + * often empty for guild profiles, get the user profile for badges + */ + badges: ProfileBadge[]; + bio: string | undefined; + popoutAnimationParticleType: string | null; + profileEffectExpiresAt: number | Date | undefined; + profileEffectId: undefined | string; + /** + * often an empty string when not set + */ + pronouns: string | "" | undefined; + themeColors: [number, number] | undefined; + userId: string; +} + +export interface ApplicationRoleConnection { + application: Application; + application_metadata: Record; + metadata: Record; + platform_name: string; + platform_username: string; +} + +export interface UserProfile extends UserProfileBase, Pick { + /** If this is a bot user profile, this will be its application */ + application: ProfileApplication | null; + applicationRoleConnections: ApplicationRoleConnection[] | undefined; + connectedAccounts: ConnectedAccount[] | undefined; + fetchStartedAt: number; + fetchEndedAt: number; + legacyUsername: string | undefined; + premiumGuildSince: Date | null; + premiumSince: Date | null; +} + +export class UserProfileStore extends FluxStore { + /** + * @param userId the user ID of the profile being fetched. + * @param guildId the guild ID to of the profile being fetched. + * defaults to the internal symbol `NO GUILD ID` if nullish + * + * @returns true if the profile is being fetched, false otherwise. + */ + isFetchingProfile(userId: string, guildId?: string): boolean; + /** + * Check if mutual friends for {@link userId} are currently being fetched. + * + * @param userId the user ID of the mutual friends being fetched. + * + * @returns true if mutual friends are being fetched, false otherwise. + */ + isFetchingFriends(userId: string): boolean; + + get isSubmitting(): boolean; + + getUserProfile(userId: string): UserProfile | undefined; + + getGuildMemberProfile(userId: string, guildId: string | undefined): UserProfileBase | null; + /** + * Get the mutual friends of a user. + * + * @param userId the user ID of the user to get the mutual friends of. + * + * @returns an array of mutual friends, or undefined if the user has no mutual friends + */ + getMutualFriends(userId: string): MutualFriend[] | undefined; + /** + * Get the count of mutual friends for a user. + * + * @param userId the user ID of the user to get the mutual friends count of. + * + * @returns the count of mutual friends, or undefined if the user has no mutual friends + */ + getMutualFriendsCount(userId: string): number | undefined; + /** + * Get the mutual guilds of a user. + * + * @param userId the user ID of the user to get the mutual guilds of. + * + * @returns an array of mutual guilds, or undefined if the user has no mutual guilds + */ + getMutualGuilds(userId: string): MutualGuild[] | undefined; +} diff --git a/packages/discord-types/src/stores/index.d.ts b/packages/discord-types/src/stores/index.d.ts index b6254844..23045832 100644 --- a/packages/discord-types/src/stores/index.d.ts +++ b/packages/discord-types/src/stores/index.d.ts @@ -11,6 +11,7 @@ export * from "./RelationshipStore"; export * from "./SelectedChannelStore"; export * from "./SelectedGuildStore"; export * from "./ThemeStore"; +export * from "./UserProfileStore"; export * from "./UserStore"; export * from "./WindowStore"; diff --git a/src/plugins/fakeProfileThemes/index.tsx b/src/plugins/fakeProfileThemes/index.tsx index 1de38ab4..5d16a842 100644 --- a/src/plugins/fakeProfileThemes/index.tsx +++ b/src/plugins/fakeProfileThemes/index.tsx @@ -22,13 +22,14 @@ import "./index.css"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; +import { fetchUserProfile } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes, copyWithToast } from "@utils/misc"; +import { useAwaiter } from "@utils/react"; import definePlugin, { OptionType } from "@utils/types"; -import { User } from "@vencord/discord-types"; +import { User, UserProfile } from "@vencord/discord-types"; import { findComponentByCodeLazy } from "@webpack"; import { Button, ColorPicker, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common"; -import { ReactElement } from "react"; import virtualMerge from "virtual-merge"; interface Colors { @@ -81,14 +82,6 @@ const settings = definePluginSettings({ } }); -interface ColorPickerProps { - color: number | null; - label: ReactElement; - showEyeDropper?: boolean; - suggestedColors?: string[]; - onChange(value: number | null): void; -} - // I can't be bothered to figure out the semantics of this component. The // functions surely get some event argument sent to them and they likely aren't // all required. If anyone who wants to use this component stumbles across this @@ -106,6 +99,100 @@ interface ProfileModalProps { const ProfileModal = findComponentByCodeLazy("isTryItOutFlow:", "pendingThemeColors:", "pendingAvatarDecoration:", "EDIT_PROFILE_BANNER"); +function SettingsAboutComponentWrapper() { + const [, , userProfileLoading] = useAwaiter(() => fetchUserProfile(UserStore.getCurrentUser().id)); + + return !userProfileLoading && ; +} + +function SettingsAboutComponent() { + const existingColors = decode( + UserProfileStore.getUserProfile(UserStore.getCurrentUser().id)?.bio ?? "" + ) ?? [0, 0]; + const [color1, setColor1] = useState(existingColors[0]); + const [color2, setColor2] = useState(existingColors[1]); + + return ( + + Usage + + After enabling this plugin, you will see custom colors in + the profiles of other people using compatible plugins.{" "} +
+ To set your own colors: +
    +
  • + • use the color pickers below to choose your colors +
  • +
  • • click the "Copy 3y3" button
  • +
  • • paste the invisible text anywhere in your bio
  • +

+ + Color pickers + + + Primary + + } + onChange={(color: number) => { + setColor1(color); + }} + /> + + Accent + + } + onChange={(color: number) => { + setColor2(color); + }} + /> + + + + Preview +
+ { }} + onBannerChange={() => { }} + canUsePremiumCustomization={true} + hideExampleButton={true} + hideFakeActivity={true} + isTryItOutFlow={true} + /> +
+
+
); +} export default definePlugin({ name: "FakeProfileThemes", @@ -117,7 +204,7 @@ export default definePlugin({ replacement: { match: /(?<=getUserProfile\(\i\){return )(.+?)(?=})/, replace: "$self.colorDecodeHook($1)" - } + }, }, { find: "#{intl::USER_SETTINGS_RESET_PROFILE_THEME}", @@ -127,97 +214,12 @@ export default definePlugin({ } } ], - settingsAboutComponent: () => { - const existingColors = decode( - UserProfileStore.getUserProfile(UserStore.getCurrentUser().id).bio - ) ?? [0, 0]; - const [color1, setColor1] = useState(existingColors[0]); - const [color2, setColor2] = useState(existingColors[1]); - return ( - - Usage - - After enabling this plugin, you will see custom colors in - the profiles of other people using compatible plugins.{" "} -
- To set your own colors: -
    -
  • - • use the color pickers below to choose your colors -
  • -
  • • click the "Copy 3y3" button
  • -
  • • paste the invisible text anywhere in your bio
  • -

- - Color pickers - - - Primary - - } - onChange={(color: number) => { - setColor1(color); - }} - /> - - Accent - - } - onChange={(color: number) => { - setColor2(color); - }} - /> - - - - Preview -
- { }} - onBannerChange={() => { }} - canUsePremiumCustomization={true} - hideExampleButton={true} - hideFakeActivity={true} - isTryItOutFlow={true} - /> -
-
-
); - }, + settingsAboutComponent: SettingsAboutComponentWrapper, + settings, - colorDecodeHook(user: User) { - if (user) { + colorDecodeHook(user: UserProfile) { + if (user?.bio) { // don't replace colors if already set with nitro if (settings.store.nitroFirst && user.themeColors) return user; const colors = decode(user.bio); diff --git a/src/plugins/showConnections/index.tsx b/src/plugins/showConnections/index.tsx index 882c869e..ca90cc48 100644 --- a/src/plugins/showConnections/index.tsx +++ b/src/plugins/showConnections/index.tsx @@ -25,7 +25,7 @@ import { CopyIcon, LinkIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { copyWithToast } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; -import { User } from "@vencord/discord-types"; +import { ConnectedAccount, User } from "@vencord/discord-types"; import { findByCodeLazy, findByPropsLazy } from "@webpack"; import { Tooltip, UserProfileStore } from "@webpack/common"; @@ -60,15 +60,8 @@ const settings = definePluginSettings({ } }); -interface Connection { - type: string; - id: string; - name: string; - verified: boolean; -} - interface ConnectionPlatform { - getPlatformUserUrl(connection: Connection): string; + getPlatformUserUrl(connection: ConnectedAccount): string; icon: { lightSVG: string, darkSVG: string; }; } @@ -88,7 +81,7 @@ function ConnectionsComponent({ id, theme }: { id: string, theme: string; }) { if (!profile) return null; - const connections: Connection[] = profile.connectedAccounts; + const connections = profile.connectedAccounts; if (!connections?.length) return null; @@ -102,7 +95,7 @@ function ConnectionsComponent({ id, theme }: { id: string, theme: string; }) { ); } -function CompactConnectionComponent({ connection, theme }: { connection: Connection, theme: string; }) { +function CompactConnectionComponent({ connection, theme }: { connection: ConnectedAccount, theme: string; }) { const platform = platforms.get(useLegacyPlatformType(connection.type)); const url = platform.getPlatformUserUrl?.(connection); diff --git a/src/plugins/validUser/index.tsx b/src/plugins/validUser/index.tsx index 4825cdaa..488b6bd6 100644 --- a/src/plugins/validUser/index.tsx +++ b/src/plugins/validUser/index.tsx @@ -22,6 +22,7 @@ import { isNonNullish } from "@utils/guards"; import { sleep } from "@utils/misc"; import { Queue } from "@utils/Queue"; import definePlugin from "@utils/types"; +import { ProfileBadge } from "@vencord/discord-types"; import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common"; import { type ComponentType, type ReactNode } from "react"; @@ -47,13 +48,6 @@ const badges: Record = { const fetching = new Set(); const queue = new Queue(5); -interface ProfileBadge { - id: string; - description: string; - icon: string; - link?: string; -} - interface MentionProps { data: { userId?: string; @@ -102,10 +96,12 @@ async function getUser(id: string) { // Fill in what we can deduce const profile = UserProfileStore.getUserProfile(id); - profile.accentColor = user.accent_color; - profile.badges = fakeBadges; - profile.banner = user.banner; - profile.premiumType = user.premium_type; + if (profile) { + profile.accentColor = user.accent_color; + profile.badges = fakeBadges; + profile.banner = user.banner; + profile.premiumType = user.premium_type; + } return userObj; } diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx index 4a652a29..d7a38ebb 100644 --- a/src/utils/discord.tsx +++ b/src/utils/discord.tsx @@ -194,7 +194,7 @@ export async function fetchUserProfile(id: string, options?: FetchUserProfileOpt }); FluxDispatcher.dispatch({ type: "USER_UPDATE", user: body.user }); - await FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_SUCCESS", ...body }); + await FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_SUCCESS", userProfile: body }); if (options?.guild_id && body.guild_member) FluxDispatcher.dispatch({ type: "GUILD_MEMBER_PROFILE_UPDATE", guildId: options.guild_id, guildMember: body.guild_member }); diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index be72d47c..4165af8e 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -42,7 +42,7 @@ export let GuildStore: t.GuildStore; export let GuildRoleStore: t.GuildRoleStore; export let GuildMemberStore: t.GuildMemberStore; export let UserStore: t.UserStore; -export let UserProfileStore: GenericStore; +export let UserProfileStore: t.UserProfileStore; export let SelectedChannelStore: t.SelectedChannelStore; export let SelectedGuildStore: t.SelectedGuildStore; export let ChannelStore: t.ChannelStore;