FakeProfileThemes: fix error when own profile is not loaded (#3514)

Co-authored-by: V <vendicated@riseup.net>
This commit is contained in:
sadan4 2025-07-10 16:38:27 -04:00 committed by dorkbutt
parent 8ec5b0a8d8
commit 8411026c51
No known key found for this signature in database
11 changed files with 297 additions and 126 deletions

View file

@ -25,3 +25,8 @@ export const enum ApplicationCommandType {
USER = 2, USER = 2,
MESSAGE = 3, MESSAGE = 3,
} }
export const enum ApplicationIntegrationType {
GUILD_INSTALL = 0,
USER_INSTALL = 1
}

View file

@ -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[];
}

View file

@ -6,7 +6,7 @@ export class User extends DiscordRecord {
constructor(user: object); constructor(user: object);
accentColor: number; accentColor: number;
avatar: string; avatar: string;
banner: string; banner: string | null | undefined;
bio: string; bio: string;
bot: boolean; bot: boolean;
desktop: boolean; desktop: boolean;
@ -27,7 +27,6 @@ export class User extends DiscordRecord {
system: boolean; system: boolean;
username: string; username: string;
verified: boolean; verified: boolean;
themeColors?: [number, number];
get createdAt(): Date; get createdAt(): Date;
get hasPremiumPerks(): boolean; get hasPremiumPerks(): boolean;

View file

@ -1,3 +1,4 @@
export * from "./Application";
export * from "./Channel"; export * from "./Channel";
export * from "./Guild"; export * from "./Guild";
export * from "./GuildMember"; export * from "./GuildMember";

View file

@ -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<string, string>;
}
export interface ProfileApplication {
id: string;
customInstallUrl: string | undefined;
installParams: ApplicationInstallParams | undefined;
flags: number;
popularApplicationCommandIds?: string[];
integrationTypesConfig: Record<ApplicationIntegrationType, Partial<{
oauth2_install_params: ApplicationInstallParams;
}>>;
primarySkuId: string | undefined;
storefront_available: boolean;
}
export interface UserProfileBase extends Pick<User, "banner"> {
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<string, any>;
metadata: Record<string, any>;
platform_name: string;
platform_username: string;
}
export interface UserProfile extends UserProfileBase, Pick<User, "premiumType"> {
/** 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;
}

View file

@ -11,6 +11,7 @@ export * from "./RelationshipStore";
export * from "./SelectedChannelStore"; export * from "./SelectedChannelStore";
export * from "./SelectedGuildStore"; export * from "./SelectedGuildStore";
export * from "./ThemeStore"; export * from "./ThemeStore";
export * from "./UserProfileStore";
export * from "./UserStore"; export * from "./UserStore";
export * from "./WindowStore"; export * from "./WindowStore";

View file

@ -22,13 +22,14 @@ import "./index.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { fetchUserProfile } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes, copyWithToast } from "@utils/misc"; import { classes, copyWithToast } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { User } from "@vencord/discord-types"; import { User, UserProfile } from "@vencord/discord-types";
import { findComponentByCodeLazy } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import { Button, ColorPicker, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common"; import { Button, ColorPicker, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
import { ReactElement } from "react";
import virtualMerge from "virtual-merge"; import virtualMerge from "virtual-merge";
interface Colors { interface Colors {
@ -81,14 +82,6 @@ const settings = definePluginSettings({
} }
}); });
interface ColorPickerProps {
color: number | null;
label: ReactElement<any>;
showEyeDropper?: boolean;
suggestedColors?: string[];
onChange(value: number | null): void;
}
// I can't be bothered to figure out the semantics of this component. The // 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 // 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 // all required. If anyone who wants to use this component stumbles across this
@ -106,6 +99,100 @@ interface ProfileModalProps {
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>("isTryItOutFlow:", "pendingThemeColors:", "pendingAvatarDecoration:", "EDIT_PROFILE_BANNER"); const ProfileModal = findComponentByCodeLazy<ProfileModalProps>("isTryItOutFlow:", "pendingThemeColors:", "pendingAvatarDecoration:", "EDIT_PROFILE_BANNER");
function SettingsAboutComponentWrapper() {
const [, , userProfileLoading] = useAwaiter(() => fetchUserProfile(UserStore.getCurrentUser().id));
return !userProfileLoading && <SettingsAboutComponent />;
}
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 (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
<Forms.FormText>
After enabling this plugin, you will see custom colors in
the profiles of other people using compatible plugins.{" "}
<br />
To set your own colors:
<ul>
<li>
use the color pickers below to choose your colors
</li>
<li> click the "Copy 3y3" button</li>
<li> paste the invisible text anywhere in your bio</li>
</ul><br />
<Forms.FormDivider
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
<Flex
direction={Flex.Direction.HORIZONTAL}
style={{ gap: "1rem" }}
>
<ColorPicker
color={color1}
label={
<Text
variant={"text-xs/normal"}
style={{ marginTop: "4px" }}
>
Primary
</Text>
}
onChange={(color: number) => {
setColor1(color);
}}
/>
<ColorPicker
color={color2}
label={
<Text
variant={"text-xs/normal"}
style={{ marginTop: "4px" }}
>
Accent
</Text>
}
onChange={(color: number) => {
setColor2(color);
}}
/>
<Button
onClick={() => {
const colorString = encode(color1, color2);
copyWithToast(colorString);
}}
color={Button.Colors.PRIMARY}
size={Button.Sizes.XLARGE}
>
Copy 3y3
</Button>
</Flex>
<Forms.FormDivider
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Preview</Forms.FormTitle>
<div className="vc-fpt-preview">
<ProfileModal
user={UserStore.getCurrentUser()}
pendingThemeColors={[color1, color2]}
onAvatarChange={() => { }}
onBannerChange={() => { }}
canUsePremiumCustomization={true}
hideExampleButton={true}
hideFakeActivity={true}
isTryItOutFlow={true}
/>
</div>
</Forms.FormText>
</Forms.FormSection>);
}
export default definePlugin({ export default definePlugin({
name: "FakeProfileThemes", name: "FakeProfileThemes",
@ -117,7 +204,7 @@ export default definePlugin({
replacement: { replacement: {
match: /(?<=getUserProfile\(\i\){return )(.+?)(?=})/, match: /(?<=getUserProfile\(\i\){return )(.+?)(?=})/,
replace: "$self.colorDecodeHook($1)" replace: "$self.colorDecodeHook($1)"
} },
}, },
{ {
find: "#{intl::USER_SETTINGS_RESET_PROFILE_THEME}", 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 ( settingsAboutComponent: SettingsAboutComponentWrapper,
<Forms.FormSection>
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
<Forms.FormText>
After enabling this plugin, you will see custom colors in
the profiles of other people using compatible plugins.{" "}
<br />
To set your own colors:
<ul>
<li>
use the color pickers below to choose your colors
</li>
<li> click the "Copy 3y3" button</li>
<li> paste the invisible text anywhere in your bio</li>
</ul><br />
<Forms.FormDivider
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
<Flex
direction={Flex.Direction.HORIZONTAL}
style={{ gap: "1rem" }}
>
<ColorPicker
color={color1}
label={
<Text
variant={"text-xs/normal"}
style={{ marginTop: "4px" }}
>
Primary
</Text>
}
onChange={(color: number) => {
setColor1(color);
}}
/>
<ColorPicker
color={color2}
label={
<Text
variant={"text-xs/normal"}
style={{ marginTop: "4px" }}
>
Accent
</Text>
}
onChange={(color: number) => {
setColor2(color);
}}
/>
<Button
onClick={() => {
const colorString = encode(color1, color2);
copyWithToast(colorString);
}}
color={Button.Colors.PRIMARY}
size={Button.Sizes.XLARGE}
>
Copy 3y3
</Button>
</Flex>
<Forms.FormDivider
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Preview</Forms.FormTitle>
<div className="vc-fpt-preview">
<ProfileModal
user={UserStore.getCurrentUser()}
pendingThemeColors={[color1, color2]}
onAvatarChange={() => { }}
onBannerChange={() => { }}
canUsePremiumCustomization={true}
hideExampleButton={true}
hideFakeActivity={true}
isTryItOutFlow={true}
/>
</div>
</Forms.FormText>
</Forms.FormSection>);
},
settings, settings,
colorDecodeHook(user: User) { colorDecodeHook(user: UserProfile) {
if (user) { if (user?.bio) {
// don't replace colors if already set with nitro // don't replace colors if already set with nitro
if (settings.store.nitroFirst && user.themeColors) return user; if (settings.store.nitroFirst && user.themeColors) return user;
const colors = decode(user.bio); const colors = decode(user.bio);

View file

@ -25,7 +25,7 @@ import { CopyIcon, LinkIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { copyWithToast } from "@utils/misc"; import { copyWithToast } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types"; 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 { findByCodeLazy, findByPropsLazy } from "@webpack";
import { Tooltip, UserProfileStore } from "@webpack/common"; 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 { interface ConnectionPlatform {
getPlatformUserUrl(connection: Connection): string; getPlatformUserUrl(connection: ConnectedAccount): string;
icon: { lightSVG: string, darkSVG: string; }; icon: { lightSVG: string, darkSVG: string; };
} }
@ -88,7 +81,7 @@ function ConnectionsComponent({ id, theme }: { id: string, theme: string; }) {
if (!profile) if (!profile)
return null; return null;
const connections: Connection[] = profile.connectedAccounts; const connections = profile.connectedAccounts;
if (!connections?.length) if (!connections?.length)
return null; 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 platform = platforms.get(useLegacyPlatformType(connection.type));
const url = platform.getPlatformUserUrl?.(connection); const url = platform.getPlatformUserUrl?.(connection);

View file

@ -22,6 +22,7 @@ import { isNonNullish } from "@utils/guards";
import { sleep } from "@utils/misc"; import { sleep } from "@utils/misc";
import { Queue } from "@utils/Queue"; import { Queue } from "@utils/Queue";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { ProfileBadge } from "@vencord/discord-types";
import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common"; import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common";
import { type ComponentType, type ReactNode } from "react"; import { type ComponentType, type ReactNode } from "react";
@ -47,13 +48,6 @@ const badges: Record<string, ProfileBadge> = {
const fetching = new Set<string>(); const fetching = new Set<string>();
const queue = new Queue(5); const queue = new Queue(5);
interface ProfileBadge {
id: string;
description: string;
icon: string;
link?: string;
}
interface MentionProps { interface MentionProps {
data: { data: {
userId?: string; userId?: string;
@ -102,10 +96,12 @@ async function getUser(id: string) {
// Fill in what we can deduce // Fill in what we can deduce
const profile = UserProfileStore.getUserProfile(id); const profile = UserProfileStore.getUserProfile(id);
profile.accentColor = user.accent_color; if (profile) {
profile.badges = fakeBadges; profile.accentColor = user.accent_color;
profile.banner = user.banner; profile.badges = fakeBadges;
profile.premiumType = user.premium_type; profile.banner = user.banner;
profile.premiumType = user.premium_type;
}
return userObj; return userObj;
} }

View file

@ -194,7 +194,7 @@ export async function fetchUserProfile(id: string, options?: FetchUserProfileOpt
}); });
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: body.user }); 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) if (options?.guild_id && body.guild_member)
FluxDispatcher.dispatch({ type: "GUILD_MEMBER_PROFILE_UPDATE", guildId: options.guild_id, guildMember: body.guild_member }); FluxDispatcher.dispatch({ type: "GUILD_MEMBER_PROFILE_UPDATE", guildId: options.guild_id, guildMember: body.guild_member });

View file

@ -42,7 +42,7 @@ export let GuildStore: t.GuildStore;
export let GuildRoleStore: t.GuildRoleStore; export let GuildRoleStore: t.GuildRoleStore;
export let GuildMemberStore: t.GuildMemberStore; export let GuildMemberStore: t.GuildMemberStore;
export let UserStore: t.UserStore; export let UserStore: t.UserStore;
export let UserProfileStore: GenericStore; export let UserProfileStore: t.UserProfileStore;
export let SelectedChannelStore: t.SelectedChannelStore; export let SelectedChannelStore: t.SelectedChannelStore;
export let SelectedGuildStore: t.SelectedGuildStore; export let SelectedGuildStore: t.SelectedGuildStore;
export let ChannelStore: t.ChannelStore; export let ChannelStore: t.ChannelStore;