Plugin
FakeProfileThemes
Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
// This plugin is a port from Alyxia's Vendetta plugin8
import "./styles.css";9
10
import { definePluginSettings } from "@api/Settings";11
import { Divider } from "@components/Divider";12
import ErrorBoundary from "@components/ErrorBoundary";13
import { Flex } from "@components/Flex";14
import { Devs } from "@utils/constants";15
import { copyWithToast, fetchUserProfile } from "@utils/discord";16
import { Margins } from "@utils/margins";17
import { classes } from "@utils/misc";18
import { useAwaiter } from "@utils/react";19
import definePlugin, { OptionType } from "@utils/types";20
import { User, UserProfile } from "@vencord/discord-types";21
import { findComponentByCodeLazy } from "@webpack";22
import { Button, ColorPicker, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";23
import virtualMerge from "virtual-merge";24
25
interface Colors {26
primary: number;27
accent: number;28
}29
30
function encode(primary: number, accent: number): string {31
const message = `[#${primary.toString(16).padStart(6, "0")},#${accent.toString(16).padStart(6, "0")}]`;32
const padding = "";33
const encoded = Array.from(message)34
.map(x => x.codePointAt(0))35
.filter(x => x! >= 0x20 && x! <= 0x7f)36
.map(x => String.fromCodePoint(x! + 0xe0000))37
.join("");38
39
return (padding || "") + " " + encoded;40
}41
42
// Courtesy of Cynthia.43
function decode(bio: string): Array<number> | null {44
if (bio == null) return null;45
46
const colorString = bio.match(47
/\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e005d}/u,48
);49
if (colorString != null) {50
const parsed = [...colorString[0]]51
.map(x => String.fromCodePoint(x.codePointAt(0)! - 0xe0000))52
.join("");53
const colors = parsed54
.substring(1, parsed.length - 1)55
.split(",")56
.map(x => parseInt(x.replace("#", "0x"), 16));57
58
return colors;59
} else {60
return null;61
}62
}63
64
const settings = definePluginSettings({65
nitroFirst: {66
description: "Default color source if both are present",67
type: OptionType.SELECT,68
options: [69
{ label: "Nitro colors", value: true, default: true },70
{ label: "Fake colors", value: false },71
]72
}73
});74
75
// I can't be bothered to figure out the semantics of this component. The76
// functions surely get some event argument sent to them and they likely aren't77
// all required. If anyone who wants to use this component stumbles across this78
// code, you'll have to do the research yourself.79
interface ProfileModalProps {80
user: User;81
pendingThemeColors: [number, number];82
onAvatarChange: () => void;83
onBannerChange: () => void;84
canUsePremiumCustomization: boolean;85
hideExampleButton: boolean;86
hideFakeActivity: boolean;87
isTryItOut: boolean;88
}89
90
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>("isTryItOut:", "pendingThemeColors:", "pendingAvatarDecoration:", "EDIT_PROFILE_BANNER");91
92
function SettingsAboutComponentWrapper() {93
const [, , userProfileLoading] = useAwaiter(() => fetchUserProfile(UserStore.getCurrentUser().id));94
95
return !userProfileLoading && <SettingsAboutComponent />;96
}97
98
function SettingsAboutComponent() {99
const existingColors = decode(100
UserProfileStore.getUserProfile(UserStore.getCurrentUser().id)?.bio ?? ""101
) ?? [0, 0];102
const [color1, setColor1] = useState(existingColors[0]);103
const [color2, setColor2] = useState(existingColors[1]);104
105
return (106
<section>107
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>108
<Forms.FormText>109
After enabling this plugin, you will see custom colors in110
the profiles of other people using compatible plugins.{" "}111
</Forms.FormText>112
<Forms.FormText className={Margins.top8}>113
<strong>To set your own profile theme colors:</strong>114
<ul>115
<li>— use the color pickers below to choose your colors</li>116
<li>— click the "Copy 3y3" button</li>117
<li>— paste the invisible text anywhere in your bio</li>118
</ul>119
<Divider120
className={classes(Margins.top8, Margins.bottom8)}121
/>122
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>123
<Flex gap="1em">124
<ColorPicker125
color={color1}126
label={127
<Text128
variant={"text-xs/normal"}129
style={{ marginTop: "4px" }}130
>131
Primary132
</Text>133
}134
onChange={(color: number) => {135
setColor1(color);136
}}137
/>138
<ColorPicker139
color={color2}140
label={141
<Text142
variant={"text-xs/normal"}143
style={{ marginTop: "4px" }}144
>145
Accent146
</Text>147
}148
onChange={(color: number) => {149
setColor2(color);150
}}151
/>152
<Button153
onClick={() => {154
const colorString = encode(color1, color2);155
copyWithToast(colorString);156
}}157
color={Button.Colors.PRIMARY}158
size={Button.Sizes.XLARGE}159
style={{ marginBottom: "auto" }}160
>161
Copy 3y3162
</Button>163
</Flex>164
<Divider165
className={classes(Margins.top8, Margins.bottom8)}166
/>167
<Forms.FormTitle tag="h3">Preview</Forms.FormTitle>168
<div className="vc-fpt-preview">169
<ProfileModal170
user={UserStore.getCurrentUser()}171
pendingThemeColors={[color1, color2]}172
onAvatarChange={() => { }}173
onBannerChange={() => { }}174
canUsePremiumCustomization={true}175
hideExampleButton={true}176
hideFakeActivity={true}177
isTryItOut={true}178
/>179
</div>180
</Forms.FormText>181
</section>);182
}183
184
export default definePlugin({185
name: "FakeProfileThemes",186
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",187
tags: ["Appearance", "Customisation"],188
authors: [Devs.Alyxia, Devs.Remty],189
patches: [190
{191
find: "UserProfileStore",192
replacement: {193
match: /(?<=getUserProfile\(\i\){return )(.+?)(?=})/,194
replace: "$self.colorDecodeHook($1)"195
},196
},197
{198
find: "#{intl::USER_SETTINGS_RESET_PROFILE_THEME}),onClick:",199
replacement: {200
match: /#{intl::USER_SETTINGS_RESET_PROFILE_THEME}\).+?}\)(?=\])(?<=color:(\i),.{0,500}?color:(\i),.{0,500}?)/,201
replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})"202
}203
}204
],205
206
settingsAboutComponent: SettingsAboutComponentWrapper,207
208
settings,209
colorDecodeHook(user: UserProfile) {210
if (user?.bio) {211
// don't replace colors if already set with nitro212
if (settings.store.nitroFirst && user.themeColors) return user;213
const colors = decode(user.bio);214
if (colors) {215
return virtualMerge(user, {216
premiumType: 2,217
themeColors: colors218
});219
}220
}221
return user;222
},223
addCopy3y3Button: ErrorBoundary.wrap(function ({ primary, accent }: Colors) {224
return <Button225
onClick={() => {226
const colorString = encode(primary, accent);227
copyWithToast(colorString);228
}}229
color={Button.Colors.PRIMARY}230
size={Button.Sizes.XLARGE}231
className={Margins.left16}232
>Copy 3y3233
</Button >;234
}, { noop: true }),235
});236