fix customProfile

This commit is contained in:
macdev
2026-06-03 20:04:14 +02:00
committed by macdev
parent fa88c05449
commit c9567f1c0a
3 changed files with 479 additions and 78 deletions
+69 -12
View File
@@ -26,6 +26,9 @@ import { userStyleRootNode, vencordRootNode } from "./Styles";
let style: HTMLStyleElement;
let themesStyle: HTMLStyleElement;
// Track active online theme <link> elements for non-blocking loading
const onlineThemeLinks = new Map<string, HTMLLinkElement>();
async function toggle(isEnabled: boolean) {
if (!style) {
if (isEnabled) {
@@ -42,10 +45,65 @@ async function toggle(isEnabled: boolean) {
style.disabled = !isEnabled;
}
/**
* Collect all active online theme URLs from both sources:
* - Settings.enabledThemeLinks (ThemeLibrary plugin)
* - Settings.themeLinks (Online Themes tab — manual URLs)
*
* Both are merged, deduplicated, and filtered by light/dark preference.
*/
function collectOnlineLinks(activeTheme: "light" | "dark" | undefined): string[] {
const { enabledThemeLinks, themeLinks } = Settings;
// Merge both arrays and deduplicate
const allRawLinks = [...new Set([...enabledThemeLinks, ...themeLinks])];
return allRawLinks
.map(rawLink => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return rawLink;
const [, mode, link] = match;
return mode === activeTheme ? link : null;
})
.filter((link): link is string => link !== null && link.trim().length > 0);
}
/**
* Apply online themes using non-blocking <link rel="stylesheet"> elements
* instead of synchronous @import, which causes Discord to freeze while
* waiting for remote CSS to download.
*
* Each URL gets its own <link> element created/removed as needed, so
* toggling a single theme only touches that one element — no full reload.
*/
function applyOnlineThemesNonBlocking(links: string[]) {
const newSet = new Set(links);
// Remove links no longer active
for (const [url, el] of onlineThemeLinks.entries()) {
if (!newSet.has(url)) {
el.remove();
onlineThemeLinks.delete(url);
}
}
// Add new links
for (const url of links) {
if (onlineThemeLinks.has(url)) continue;
const el = document.createElement("link");
el.rel = "stylesheet";
el.type = "text/css";
el.href = url.trim();
// Append to userStyleRootNode so it lives in our isolated style tree
userStyleRootNode.appendChild(el);
onlineThemeLinks.set(url, el);
}
}
async function initThemes() {
themesStyle ??= createAndAppendStyle("vencord-themes", userStyleRootNode);
const { enabledThemeLinks, enabledThemes } = Settings;
const { enabledThemes } = Settings;
const { ThemeStore } = require("@webpack/common/stores") as typeof import("@webpack/common/stores");
@@ -55,29 +113,26 @@ async function initThemes() {
? undefined
: ThemeStore.theme === "light" ? "light" : "dark";
const links = enabledThemeLinks
.map(rawLink => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return rawLink;
// --- Online themes: non-blocking <link> approach (no freeze on slow CDN) ---
const onlineLinks = collectOnlineLinks(activeTheme);
applyOnlineThemesNonBlocking(onlineLinks);
const [, mode, link] = match;
return mode === activeTheme ? link : null;
})
.filter(link => link !== null);
// --- Local / desktop themes: @import is fine (vencord:// = local file, instant) ---
const localImports: string[] = [];
if (IS_WEB) {
for (const theme of enabledThemes) {
const themeData = await VencordNative.themes.getThemeData(theme);
if (!themeData) continue;
const blob = new Blob([themeData], { type: "text/css" });
links.push(URL.createObjectURL(blob));
localImports.push(URL.createObjectURL(blob));
}
} else {
const localThemes = enabledThemes.map(theme => `vencord:///themes/${theme}?v=${Date.now()}`);
links.push(...localThemes);
localImports.push(...localThemes);
}
themesStyle.textContent = links.map(link => `@import url("${link.trim()}");`).join("\n");
themesStyle.textContent = localImports.map(link => `@import url("${link.trim()}");`).join("\n");
updatePopoutWindows();
}
@@ -109,7 +164,9 @@ document.addEventListener("DOMContentLoaded", () => {
toggle(Settings.useQuickCss);
SettingsStore.addChangeListener("useQuickCss", toggle);
// Listen to ALL theme-related settings so any change triggers a reload
SettingsStore.addChangeListener("enabledThemeLinks", initThemes);
SettingsStore.addChangeListener("themeLinks", initThemes);
SettingsStore.addChangeListener("enabledThemes", initThemes);
window.addEventListener("message", event => {
+405 -61
View File
@@ -12,10 +12,10 @@ import { addHeaderBarButton, HeaderBarButton, removeHeaderBarButton } from "@api
import { DataStore } from "@api/index";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { AuthenticationStore, Button, FluxDispatcher, IconUtils, Menu, React, Select, SettingsRouter, SnowflakeUtils,UserStore } from "@webpack/common";
import { AuthenticationStore, Button, FluxDispatcher, IconUtils, Menu, OAuth2AuthorizeModal, React, Select, SettingsRouter, SnowflakeUtils, UserStore } from "@webpack/common";
import { Settings } from "@api/Settings";
import { getPublicPluginConfig, saveOwnPluginConfig } from "../../api/PluginSync";
import { getStoredToken } from "../../api/OAuth2";
import { getStoredToken, storeToken, beginDiscordOAuth, API_BASE } from "../../api/OAuth2";
import virtualMerge from "virtual-merge";
import { t } from "../autoTranslateNightcord";
@@ -62,7 +62,7 @@ const NITRO_LEVELS = [
{ label: t("Or (3 mois)"), icon: "https://cdn.discordapp.com/badge-icons/2895086c18d5531d499862e41d1155a6.png" },
{ label: t("Platine (6 mois)"), icon: "https://cdn.discordapp.com/badge-icons/0334688279c8359120922938dcb1d6f8.png" },
{ label: t("Diamant (12 mois)"), icon: "https://cdn.discordapp.com/badge-icons/0d61871f72bb9a33a7ae568c1fb4f20a.png" },
{ label: t("Émeraude (24 mois)"), icon: "https://cdn.discordapp.com/badge-icons/11e2d339068b55d3a506cff34d3780f3.png" },
{ label: t("Émeraude (24 mois)"), icon: "https://cdn.discordapp.com/badge-icons/11e2d339068b55d3a506cff34d3780f3.png" },
{ label: t("Rubis (36 mois)"), icon: "https://cdn.discordapp.com/badge-icons/cd5e2cfd9d7f27a8cdcd3e8a8d5dc9f4.png" },
{ label: t("Opale (72 mois)"), icon: "https://cdn.discordapp.com/badge-icons/5b154df19c53dce2af92c9b61e6be5e2.png" },
];
@@ -148,21 +148,46 @@ let storedData: CustomProfileData = {};
let isEnabled = false;
let domObserver: MutationObserver | null = null;
const publicProfilesCache = new Map<string, { fetched: boolean, data: CustomProfileData | null }>();
const publicProfilesCache = new Map<string, { fetched: boolean, data: CustomProfileData | null, timestamp: number }>();
const PUBLIC_CACHE_TTL = 1000 * 30; // 30 seconds — fast enough to see updates without hammering the API
// Watch for seeAllCustomProfile being toggled off — flush the cache immediately
let _lastSeeAll = false;
function checkSeeAllSettingChange() {
const current = !!Settings.seeAllCustomProfile;
if (_lastSeeAll && !current) {
// Turned off — wipe all public profile data so everyone shows their real profile
publicProfilesCache.clear();
}
_lastSeeAll = current;
}
async function fetchPublicProfileIfNeeded(userId: string) {
checkSeeAllSettingChange();
if (!Settings.seeAllCustomProfile) return;
if (publicProfilesCache.has(userId)) return;
publicProfilesCache.set(userId, { fetched: false, data: null });
const existing = publicProfilesCache.get(userId);
if (existing?.fetched && (Date.now() - existing.timestamp) < PUBLIC_CACHE_TTL) return;
publicProfilesCache.set(userId, { fetched: false, data: null, timestamp: 0 });
const result = await getPublicPluginConfig("customProfile", userId);
publicProfilesCache.set(userId, { fetched: true, data: result?.settings || null });
const dataToSave = result?.settings || null;
if (dataToSave) {
delete dataToSave.username;
delete dataToSave.globalName;
delete dataToSave.avatar;
delete dataToSave.bio;
delete dataToSave.pronouns;
delete dataToSave.email;
delete dataToSave.phone;
delete dataToSave.copiedUserId;
}
publicProfilesCache.set(userId, { fetched: true, data: dataToSave, timestamp: Date.now() });
try {
const UPS = (Vencord as any).Webpack?.findByProps?.("getUserProfile", "getGuildMemberProfile");
if (UPS && UPS.emitChange) UPS.emitChange();
const US = (Vencord as any).Webpack?.findByStoreName("UserStore");
if (US && US.emitChange) US.emitChange();
} catch {}
@@ -272,23 +297,36 @@ function removeHideStyle() {
if (isEnabled) injectHideStyle();
let _avatarPatchApplied = false;
let _avatarPatchOrig: any = null;
function applyAvatarPatchEarly() {
if (_avatarPatchApplied || !isEnabled || !storedData.avatar) return;
if (_avatarPatchApplied) return;
try {
const IU = (window as any).Vencord?.Webpack?.findByProps?.("getUserAvatarURL");
// findByProps is more reliable than the imported IconUtils as it returns
// the actual live webpack module object — patching it affects all consumers.
const IU = (window as any).Vencord?.Webpack?.findByProps?.("getUserAvatarURL", "getDefaultAvatarURL")
?? (window as any).Vencord?.Webpack?.findByProps?.("getUserAvatarURL")
?? IconUtils;
if (!IU?.getUserAvatarURL) return;
const orig = IU.getUserAvatarURL;
_avatarPatchOrig = IU.getUserAvatarURL;
const orig = _avatarPatchOrig;
// The patch reads storedData/isEnabled at call-time, not at install-time
// so it works even if called before loadData() finishes.
IU.getUserAvatarURL = function (user: any, ...args: any[]) {
if (!user) return orig(user, ...args);
const uid = user.id ?? user.userId;
if (isEnabled && storedData.avatar && uid && isMe(uid)) {
if (!uid) return orig(user, ...args);
// Own user
if (isEnabled && storedData.avatar && isMe(uid)) {
return storedData.avatar;
}
if (Settings.seeAllCustomProfile && uid) {
// Other users via public cache
checkSeeAllSettingChange();
if (Settings.seeAllCustomProfile && !isMe(uid)) {
const cached = publicProfilesCache.get(uid);
if (cached?.fetched && cached.data?.avatar) {
return cached.data.avatar;
}
fetchPublicProfileIfNeeded(uid);
}
return orig(user, ...args);
};
@@ -717,7 +755,7 @@ function BadgePicker({ selected, onChange, nitroType, onNitroType, boostLevel, o
icon="https://cdn.discordapp.com/badge-icons/7d9ae358c8c5e118768335dbe68b4fb8.png"
active={customIds.includes("quest")}
onClick={() => onCustomIds(customIds.includes("quest") ? customIds.filter(x => x !== "quest") : [...customIds, "quest"])} />
<BadgeBtn label={t("Orbs — Apprentice")}
<BadgeBtn label={t("Orbs Apprentice")}
icon="https://cdn.discordapp.com/badge-icons/83d8a1eb09a8d64e59233eec5d4d5c2d.png"
active={customIds.includes("orbs")}
onClick={() => onCustomIds(customIds.includes("orbs") ? customIds.filter(x => x !== "orbs") : [...customIds, "orbs"])} />
@@ -730,7 +768,7 @@ function BadgePicker({ selected, onChange, nitroType, onNitroType, boostLevel, o
<input className="cp-input" value={oldName} placeholder="OldUser#0000"
onChange={e => onOldName(e.target.value)} />
<div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 3 }}>
{t('Ex : Triggerr#5954 — will appear as "Old username: Triggerr#5954" when hovering the badge.')}
{t('Ex : Triggerr#5954 will appear as "Old username: Triggerr#5954" when hovering the badge.')}
</div>
</div>
)}
@@ -798,7 +836,7 @@ function CustomProfileModal({ rootProps }: { rootProps: any; }) {
} catch (e) { console.error("[CustomProfile] Failed to fetch accounts:", e); }
const me = UserStore.getCurrentUser();
// Pour debug: si on ne trouve qu'un compte, on simule quand même pour voir si la barre s'affiche
// Pour debug: si on ne trouve qu'un compte, on simule quand même pour voir si la barre s'affiche
return me ? [me, { ...me, id: "debug-placeholder", username: "Second Account?", globalName: "Simulation" }] : [];
}, []);
@@ -831,11 +869,53 @@ function CustomProfileModal({ rootProps }: { rootProps: any; }) {
_dataVersion++;
if (Settings.syncOwnCustomProfile) {
getStoredToken().then(t => {
if (t) {
saveOwnPluginConfig("customProfile", t, savedData as Record<string, unknown>).catch(e => {
const dataToSync = { ...savedData };
delete dataToSync.username;
delete dataToSync.globalName;
delete dataToSync.avatar;
delete dataToSync.bio;
delete dataToSync.pronouns;
delete dataToSync.email;
delete dataToSync.phone;
delete dataToSync.copiedUserId;
getStoredToken().then(token => {
if (token) {
// private: false ensures others can fetch it via /public endpoint
saveOwnPluginConfig("customProfile", token, { ...dataToSync, private: false }).then(() => {
// Invalidate our own cache so others see updated data immediately
publicProfilesCache.delete(myId);
}).catch(e => {
console.error("[CustomProfile] Failed to sync to cloud:", e);
});
} else {
// No token yet — open OAuth to get one, then sync
beginDiscordOAuth().then(oauthData => {
const clientId = new URL(oauthData.url).searchParams.get("client_id") ?? "";
openModal((p: any) => <OAuth2AuthorizeModal
{...p}
scopes={oauthData.scopes}
responseType="code"
redirectUri={oauthData.redirectUri}
permissions={0n}
clientId={clientId}
cancelCompletesFlow={false}
callback={async ({ location }: { location: string }) => {
try {
const res = await fetch(location);
const json = await res.json();
if (json?.token) {
await storeToken(json.token);
saveOwnPluginConfig("customProfile", json.token, { ...dataToSync, private: false }).then(() => {
publicProfilesCache.delete(myId);
}).catch(e => console.error("[CustomProfile] Failed to sync after OAuth:", e));
}
} catch (e) {
console.error("[CustomProfile] OAuth callback error:", e);
}
}}
/>);
}).catch(e => console.error("[CustomProfile] OAuth initiation failed:", e));
}
});
}
@@ -868,6 +948,17 @@ function CustomProfileModal({ rootProps }: { rootProps: any; }) {
cachedOriginalUser = null;
_trueOriginalUser = null;
_dataVersion++;
// Push private:true to server so others immediately stop seeing the profile
if (Settings.syncOwnCustomProfile) {
getStoredToken().then(token => {
if (token) {
saveOwnPluginConfig("customProfile", token, { private: true }).catch(() => {});
// Also clear our own entry from public cache
publicProfilesCache.delete(myId);
}
});
}
}
saveAllDataSync();
@@ -935,7 +1026,7 @@ function CustomProfileModal({ rootProps }: { rootProps: any; }) {
<Field label={t("Bio")} value={data.bio ?? ""} placeholder={t("My description...")} onChange={v => set("bio", v)} />
<Field label={t("Pronouns")} value={data.pronouns ?? ""} placeholder={t("he/him")} onChange={v => set("pronouns", v)} />
<div className="cp-field">
<SectionLabel>{t("Profile color (Nitro — gradient possible)")}</SectionLabel>
<SectionLabel>{t("Profile color (Nitro gradient possible)")}</SectionLabel>
<div className="cp-color-row" style={{ marginBottom: 6 }}>
<span style={{ fontSize: 11, color: "var(--text-muted)", marginRight: 6 }}>{t("Color 1")}</span>
<input type="color" value={accentHex || "#5865f2"} onChange={e => { const n = parseInt(e.target.value.replace("#", ""), 16); if (!isNaN(n)) set("accentColor", n); }} className="cp-color-swatch" />
@@ -1007,7 +1098,7 @@ function CustomProfileModal({ rootProps }: { rootProps: any; }) {
}
}}
>
Share your Custom Profile with everyone â click to manage in Nightcord Settings
Share your Custom Profile with everyone click to manage in Nightcord Settings
</a>
</div>
</ModalContent>
@@ -1024,6 +1115,69 @@ function CustomProfileButton() {
return <HeaderBarButton icon={() => <EditIcon size={18} />} tooltip="Custom Profile" onClick={() => openModal(props => <CustomProfileModal rootProps={props} />)} />;
}
function CPDMNotice({ userId }: { userId: string; }) {
const cached = publicProfilesCache.get(userId);
// Only show if the user has actually modified something visible in their profile
const data = cached?.fetched ? cached?.data : null;
const hasRealModifications = data && (
data.username || data.globalName || data.avatar || data.banner ||
data.bio || data.pronouns || data.accentColor != null ||
data.badgeFlags || data.nitro || data.decorationAsset ||
(data.customBadgeIds && data.customBadgeIds.length > 0) ||
data.createdAt
);
const [showRaw, setShowRaw] = React.useState(false);
if (!Settings.seeAllCustomProfile || !hasRealModifications) return null;
return (
<div style={{
margin: "8px 0 12px 0",
padding: "10px 14px",
background: "rgba(250, 166, 26, 0.1)",
border: "1px solid rgba(250, 166, 26, 0.4)",
borderRadius: 6,
display: "flex",
alignItems: "flex-start",
gap: 10,
}}>
<span style={{ fontSize: 18, flexShrink: 0 }}></span>
<div style={{ flex: 1 }}>
<span style={{ color: "var(--text-warning, #faa61a)", fontWeight: 600, fontSize: 13 }}>
WARNING This user has CustomProfile enabled. Their profile has been modified.
</span>
<br />
<span
role="button"
style={{ color: "var(--text-link)", fontSize: 12, cursor: "pointer", marginTop: 2, display: "inline-block" }}
onClick={() => setShowRaw(r => !r)}
>
{showRaw ? "Hide raw profile" : "View raw profile"}
</span>
{showRaw && (() => {
const data = cached!.data!;
const fields: [string, string][] = [];
if (data.username) fields.push(["Username", data.username]);
if (data.globalName) fields.push(["Display name", data.globalName]);
if (data.bio) fields.push(["Bio", data.bio]);
if (data.pronouns) fields.push(["Pronouns", data.pronouns]);
if (data.createdAt) fields.push(["Account created", data.createdAt]);
if (data.nitro) fields.push(["Nitro", "Simulated"]);
return (
<div style={{ marginTop: 6, fontSize: 12, color: "var(--text-muted)", display: "flex", flexDirection: "column", gap: 2 }}>
{fields.map(([k, v]) => (
<span key={k}><strong>{k}:</strong> {v}</span>
))}
</div>
);
})()}
</div>
</div>
);
}
function fakeUser(user: any): any {
if (!user) return user;
try {
@@ -1036,19 +1190,28 @@ function fakeUser(user: any): any {
export default definePlugin({
name: "CustomProfile",
enabledByDefault: true,
description: t("Visually customize your Discord profile (username, PFP, banner, badges, bio...) — persistent, only visible to you."),
description: t("Visually customize your Discord profile (username, PFP, banner, badges, bio...) persistent, only visible to you."),
authors: [{ name: "Nightcord", id: 0n }],
dependencies: ["HeaderBarAPI", "ContextMenuAPI"],
patches: [
{
find: ':"SHOULD_LOAD");',
// Inject the CustomProfile warning notice into DM welcome screens
find: "getRecipientId()",
noWarn: true,
replacement: {
match: /(children:\[)(\i\.isDM\(\).{0,300})/,
replace: "$1$self.renderDMNotice(this.props),$2"
}
},
{
find: '"SHOULD_LOAD");',
replacement: {
match: /\i(?:\?)?.getPreviewBanner\(\i,\i,\i\)(?=.{0,100}"COMPLETE")/,
replace: "$self.patchBannerUrl(arguments[0])||$&"
}
},
// UserProfileStore patch removed — caused invisible channels for members
// UserProfileStore patch removed caused invisible channels for members
// with high permissions. getUserProfile is called by Discord to calculate
// VIEW_CHANNEL and other permissions. virtualMerge with premiumType:2 corrupted
// these calculations even with isMe() guard. DomObserver + fakeCurrentUser are enough.
@@ -1245,8 +1408,8 @@ export default definePlugin({
clone.premiumGuildSince = null;
}
} else if (isEnabled) {
// Si le plugin est activé mais Nitro simulation OFF
// On force la suppression des badges Nitro/Boost si demandés ou si simulés par erreur
// Si le plugin est activé mais Nitro simulation OFF
// On force la suppression des badges Nitro/Boost si demandés ou si simulés par erreur
if (storedData.nitro === false) {
clone.premiumType = 0;
clone.premiumSince = null;
@@ -1272,9 +1435,18 @@ export default definePlugin({
if (!realUser || !realUser.id) return realUser;
const clone = Object.create(realUser);
// Username / display name
if (data.username) clone.username = data.username;
if (data.globalName) clone.globalName = data.globalName;
// Avatar — override directly on the clone object
if (data.avatar) clone.avatar = data.avatar;
if (data.email) clone.email = data.email;
if (data.phone) clone.phone = data.phone;
// Account creation date — must override createdAt AND store the id
// so SnowflakeUtils.extractTimestamp gets intercepted per-user
if (data.createdAt) {
const fakeCreatedAt = new Date(data.createdAt + "T12:00:00Z");
Object.defineProperty(clone, "createdAt", {
@@ -1282,6 +1454,8 @@ export default definePlugin({
configurable: true,
enumerable: true
});
// Tag this clone so extractTimestamp can return the fake timestamp
clone.__cp_fakeCreatedAt = fakeCreatedAt.getTime();
}
if (data.decorationAsset) {
@@ -1381,7 +1555,7 @@ export default definePlugin({
const badgesArr = Array.isArray(profile.badges) ? [...profile.badges] : [];
const customIds = data.customBadgeIds ?? [];
if (customIds.includes("quest")) badgesArr.push({ id: "quest", icon: "7d9ae358c8c5e118768335dbe68b4fb8", description: "Completed a quest" });
if (customIds.includes("orbs")) badgesArr.push({ id: "orbs", icon: "83d8a1eb09a8d64e59233eec5d4d5c2d", description: "Orbs — Apprentice" });
if (customIds.includes("orbs")) badgesArr.push({ id: "orbs", icon: "83d8a1eb09a8d64e59233eec5d4d5c2d", description: "Orbs Apprentice" });
if (customIds.includes("oldname")) {
const dText = data.oldName ? "Originally known as " + data.oldName : "Originally known as ...";
badgesArr.push({ id: "legacy_username", icon: "6de6d34650760ba5551a79732e98ed60", description: dText });
@@ -1449,11 +1623,11 @@ export default definePlugin({
merged.premiumGuildSince = null;
}
// On s'assure que les badges originaux sont écrasés dans le profil
// On s'assure que les badges originaux sont écrasés dans le profil
merged.publicFlags = (storedData.badgeFlags != null) ? storedData.badgeFlags : profile.publicFlags;
merged.badges = []; // Force Discord à recalculer la liste à partir de publicFlags et premiumType
merged.badges = []; // Force Discord à recalculer la liste à partir de publicFlags et premiumType
} else if (isEnabled && storedData.nitro === false) {
// Si Nitro simulation est OFF, on force la suppression des badges simulés
// Si Nitro simulation est OFF, on force la suppression des badges simulés
merged.premiumType = profile.premiumType ?? 0;
merged.premiumSince = profile.premiumSince ?? null;
merged.premiumGuildSince = profile.premiumGuildSince ?? null;
@@ -1490,9 +1664,45 @@ export default definePlugin({
return "***-***-" + fake.slice(-4);
},
renderDMNotice(props: any) {
try {
if (!Settings.seeAllCustomProfile) return null;
const channel = props?.channel;
if (!channel?.isDM?.()) return null;
const recipientId = channel.recipients?.[0];
if (!recipientId) return null;
fetchPublicProfileIfNeeded(recipientId);
const cached = publicProfilesCache.get(recipientId);
if (!cached?.fetched || !cached?.data) return null;
const d = cached.data;
const hasRealModifications = d.username || d.globalName || d.avatar || d.banner ||
d.bio || d.pronouns || d.accentColor != null || d.badgeFlags ||
d.nitro || d.decorationAsset || (d.customBadgeIds && d.customBadgeIds.length > 0) || d.createdAt;
if (!hasRealModifications) return null;
return <CPDMNotice userId={recipientId} />;
} catch { return null; }
},
patchBannerUrl({ displayProfile }: any) {
if (!isEnabled || !storedData.nitro || !storedData.banner) return null;
try { return isMe(displayProfile?.userId) ? storedData.banner : null; } catch { return null; }
try {
const uid = displayProfile?.userId;
if (!uid) return null;
// Own user
if (isEnabled && storedData.nitro && storedData.banner && isMe(uid)) {
return storedData.banner;
}
// Other users via public cache
checkSeeAllSettingChange();
if (Settings.seeAllCustomProfile) {
const cached = publicProfilesCache.get(uid);
if (cached?.fetched && cached.data?.banner && cached.data?.nitro) {
return cached.data.banner;
}
}
return null;
} catch { return null; }
},
toolboxActions: {
@@ -1511,9 +1721,19 @@ export default definePlugin({
// Auto-sync own profile to cloud on startup if option enabled
loadData().then(() => {
if (Settings.syncOwnCustomProfile && storedData && Object.keys(storedData).length > 0) {
const dataToSync = { ...storedData };
delete dataToSync.username;
delete dataToSync.globalName;
delete dataToSync.avatar;
delete dataToSync.bio;
delete dataToSync.pronouns;
delete dataToSync.email;
delete dataToSync.phone;
delete dataToSync.copiedUserId;
getStoredToken().then(t => {
if (t) {
saveOwnPluginConfig("customProfile", t, storedData as Record<string, unknown>).catch(e => {
saveOwnPluginConfig("customProfile", t, { ...dataToSync, private: false }).catch(e => {
console.error("[CustomProfile] Auto-sync on startup failed:", e);
});
}
@@ -1564,6 +1784,9 @@ export default definePlugin({
return this.fakeCurrentUser(user);
}
// Check if seeAll was just turned off and clear cache if needed
checkSeeAllSettingChange();
if (Settings.seeAllCustomProfile) {
const cached = publicProfilesCache.get(id);
if (cached?.fetched && cached.data) {
@@ -1577,6 +1800,28 @@ export default definePlugin({
}
} catch { }
// INTERCEPTION ON GuildMemberStore (for server member list nickname + avatar)
try {
const GMS = (Vencord as any).Webpack?.findByProps?.("getMember", "getMembers", "getMemberIds");
if (GMS && !GMS._cp_member_hook) {
const origGetMember = GMS.getMember.bind(GMS);
GMS.getMember = (guildId: string, userId: string) => {
const member = origGetMember(guildId, userId);
if (!member) return member;
// Only patch own user — never expose custom nick to other users' views
if (isEnabled && isMe(userId)) {
const patched = { ...member };
if (storedData.username) patched.nick = storedData.globalName || storedData.username;
return patched;
}
return member;
};
GMS._cp_member_hook = true;
}
} catch { }
// INTERCEPTION ON UserProfileStore (for native Nitro/Boost badges in popout/modal profile)
try {
const UPS = (Vencord as any).Webpack?.findByProps?.("getUserProfile", "getGuildMemberProfile");
@@ -1673,15 +1918,23 @@ export default definePlugin({
}
} catch { }
// Patch SnowflakeUtils.extractTimestamp pour faker la date de création
// Patch SnowflakeUtils.extractTimestamp pour faker la date de création
try {
if (SnowflakeUtils?.extractTimestamp && !this._origExtractTimestamp) {
this._origExtractTimestamp = SnowflakeUtils.extractTimestamp;
const origExtract = this._origExtractTimestamp;
(SnowflakeUtils as any).extractTimestamp = (snowflake: string) => {
// Own user
if (isEnabled && storedData.createdAt && isMe(snowflake)) {
return new Date(storedData.createdAt + "T12:00:00Z").getTime();
}
// Other users via public cache
if (Settings.seeAllCustomProfile) {
const cached = publicProfilesCache.get(snowflake);
if (cached?.fetched && cached.data?.createdAt) {
return new Date(cached.data.createdAt + "T12:00:00Z").getTime();
}
}
return origExtract(snowflake);
};
}
@@ -1689,7 +1942,13 @@ export default definePlugin({
loadData().then(() => {
updateCachedRealData();
applyAvatarPatchEarly();
// Retry avatar patch — may have failed at early boot if module wasn't ready yet
if (!_avatarPatchApplied) {
applyAvatarPatchEarly();
} else {
// Module already patched but storedData was empty at patch time — the patch
// reads storedData at call-time so no re-patch needed, just rerender.
}
if (isEnabled) {
forceAccountPanelRerender();
requestAnimationFrame(() => removeHideStyle());
@@ -1698,15 +1957,17 @@ export default definePlugin({
}
});
// Patch getAvatarDecorationURL pour injecter notre déco uniquement sur notre user
// Patch getAvatarDecorationURL pour injecter notre déco uniquement sur notre user
try {
const decoMod = (Vencord as any).Webpack?.findByProps?.("getAvatarDecorationURL");
if (decoMod?.getAvatarDecorationURL) {
const origDeco = decoMod.getAvatarDecorationURL.bind(decoMod);
decoMod.getAvatarDecorationURL = (opts: any) => {
try {
const { avatarDecoration, userId, canAnimate } = opts ?? {};
// Own user decoration
if (isEnabled && storedData.decorationAsset) {
const { avatarDecoration, userId, canAnimate } = opts ?? {};
const myId = UserStore.getCurrentUser()?.id;
const isOurs = (avatarDecoration?.skuId === "__fake__")
|| (avatarDecoration?.asset === storedData.decorationAsset)
@@ -1714,7 +1975,18 @@ export default definePlugin({
if (isOurs) {
const asset = storedData.decorationAsset;
const dec = AVATAR_DECORATIONS.find(d => d.id === asset);
const passthrough = dec ? dec.passthrough : asset.startsWith("a_");
const passthrough = dec ? (dec as any).passthrough : asset.startsWith("a_");
return getDecorationUrl(asset, passthrough);
}
}
// Other users via public cache
if (Settings.seeAllCustomProfile && userId) {
const cached = publicProfilesCache.get(userId);
if (cached?.fetched && cached.data?.decorationAsset) {
const asset = cached.data.decorationAsset!;
const dec = AVATAR_DECORATIONS.find(d => d.id === asset);
const passthrough = dec ? (dec as any).passthrough : asset.startsWith("a_");
return getDecorationUrl(asset, passthrough);
}
}
@@ -1724,28 +1996,91 @@ export default definePlugin({
}
} catch { }
// Avatar patch is already applied by applyAvatarPatchEarly() above.
// We only apply this fallback if the early patch somehow missed.
if (IconUtils?.getUserAvatarURL && !_avatarPatchApplied) {
this._origGetUserAvatarURL = IconUtils.getUserAvatarURL;
const orig = this._origGetUserAvatarURL;
(IconUtils as any).getUserAvatarURL = (user: any, ...args: any[]) => {
if (isEnabled && storedData.avatar) {
const uid = user?.id ?? user?.userId;
if (uid && isMe(uid)) return storedData.avatar;
}
return orig(user, ...args);
};
_avatarPatchApplied = true;
applyAvatarPatchEarly();
}
// Hook GuildMemberStore.getMember — only patches nick for own user
try {
const GMS = (Vencord as any).Webpack?.findByProps?.("getMember", "getMembers", "getMemberIds");
if (GMS?.getMember && !GMS._cp_member_hook) {
const _origGetMember = GMS.getMember.bind(GMS);
GMS.getMember = (guildId: string, userId: string) => {
const member = _origGetMember(guildId, userId);
try {
const myId = UserStore.getCurrentUser()?.id;
// Only patch our own member entry
if (isEnabled && userId === myId && member) {
const customNick = storedData.globalName || storedData.username;
if (customNick) {
return { ...member, nick: customNick };
}
}
} catch { }
return member;
};
GMS._cp_member_hook = true;
GMS._cp_orig_getMember = _origGetMember;
}
} catch { }
},
userProfileBadges: [
{
getBadges({ userId, badges: nativeBadges }: { userId: string; guildId: string; badges: ProfileBadge[]; }) {
const style = { borderRadius: "50%", width: "22px", height: "22px" };
// --- Other users via public cache ---
const isCurrentUser = userId === UserStore.getCurrentUser()?.id;
if (!isCurrentUser) {
if (!Settings.seeAllCustomProfile) return nativeBadges || [];
const cached = publicProfilesCache.get(userId);
if (!cached?.fetched || !cached.data) return nativeBadges || [];
const d = cached.data;
let badges: ProfileBadge[] = [...(nativeBadges || [])].filter(b => {
const desc = (b.description || "").toLowerCase();
const icon = (b.iconSrc || "").toLowerCase();
const nitroKw = ["nitro", "subscriber", "abonn", "premium", "inscrit"];
if (nitroKw.some(k => desc.includes(k))) return false;
if (icon.includes("nitro") || icon.includes("premium")) return false;
const boostKw = ["booster", "boost"];
if (boostKw.some(k => desc.includes(k))) return false;
if (icon.includes("boost") || icon.includes("leveling")) return false;
return true;
});
const extra: ProfileBadge[] = [];
const wantedFlags = d.badgeFlags ?? 0;
for (const badge of BADGES) {
if (wantedFlags & badge.flag) {
extra.push({ description: badge.label, iconSrc: badge.icon, position: 0, props: { style } });
}
}
const nl = d.nitroLevel ?? -1;
if (nl >= 0 && nl < NITRO_LEVELS.length) {
extra.push({ description: "Nitro", iconSrc: NITRO_LEVELS[nl].icon, position: 0, props: { style } });
}
const bm = d.boostMonths ?? -1;
if (bm >= 0 && bm < BOOST_ICONS.length) {
extra.push({ description: `Server Booster \u2014 ${BOOST_LABELS[bm]}`, iconSrc: BOOST_ICONS[bm], position: 0, props: { style } });
}
if (d.customBadgeIds?.includes("quest")) extra.push({ description: "Completed a quest", iconSrc: "https://cdn.discordapp.com/badge-icons/7d9ae358c8c5e118768335dbe68b4fb8.png", position: 0, props: { style } });
if (d.customBadgeIds?.includes("orbs")) extra.push({ description: "Orbs \u2014 Apprentice", iconSrc: "https://cdn.discordapp.com/badge-icons/83d8a1eb09a8d64e59233eec5d4d5c2d.png", position: 0, props: { style } });
if (d.customBadgeIds?.includes("oldname")) {
const oldNameText = d.oldName ? `Old username: ${d.oldName}` : "Old username";
extra.push({ description: oldNameText, iconSrc: OLD_NAME_BADGE_ICON, position: 0, props: { style } });
}
badges.push(...extra);
return badges;
}
// --- Own user ---
if (!isEnabled) return nativeBadges || [];
if (userId !== UserStore.getCurrentUser()?.id) return nativeBadges || [];
let badges: ProfileBadge[] = [...(nativeBadges || [])];
const style = { borderRadius: "50%", width: "22px", height: "22px" };
// Determine which fake badges are active to filter real ones (avoid duplicates)
const nl = storedData.nitroLevel ?? -1;
@@ -1772,11 +2107,13 @@ export default definePlugin({
// Logic for other flags (Staff, Partner, HypeSquad, etc.)
for (const badge of BADGES) {
if (wantedFlags & badge.flag) {
// Match on CDN icon hash (reliable across all locales)
const iconParts = badge.icon.split("/");
const iconHash = iconParts[iconParts.length - 1].replace(".png", "");
if (icon.includes(iconHash)) return false;
// Fallback: match EN keywords from the CDN URL path
const badgeKeywords = badge.label.toLowerCase().split(" ");
if (badgeKeywords.some(k => k.length > 3 && desc.includes(k))) return false;
const iconParts = badge.icon.split("/");
const iconName = iconParts[iconParts.length - 1];
if (icon.includes(iconName)) return false;
}
}
@@ -1843,7 +2180,7 @@ export default definePlugin({
// 11. SERVER BOOST (Right after Early Supporter on image 2)
if (hasBoostFake) {
badgeList.push({ description: `Server Booster — ${BOOST_LABELS[bm]}`, iconSrc: BOOST_ICONS[bm], position: 0, props: { style, title: `Server Booster — ${BOOST_LABELS[bm]}` } });
badgeList.push({ description: `Server Booster ${BOOST_LABELS[bm]}`, iconSrc: BOOST_ICONS[bm], position: 0, props: { style, title: `Server Booster ${BOOST_LABELS[bm]}` } });
}
// 12. Active Developer
@@ -1857,14 +2194,14 @@ export default definePlugin({
badgeList.push({ description: oldNameText, iconSrc: OLD_NAME_BADGE_ICON, position: 0, props: { style, title: oldNameText } });
}
// 14. Completed Quest (Quêtes)
// 14. Completed Quest (Quêtes)
if (storedData.customBadgeIds?.includes("quest")) {
badgeList.push({ description: "Completed a quest", iconSrc: "https://cdn.discordapp.com/badge-icons/7d9ae358c8c5e118768335dbe68b4fb8.png", position: 0, props: { style } });
}
// 15. Orbs
if (storedData.customBadgeIds?.includes("orbs")) {
badgeList.push({ description: "Orbs — Apprentice", iconSrc: "https://cdn.discordapp.com/badge-icons/83d8a1eb09a8d64e59233eec5d4d5c2d.png", position: 0, props: { style } });
badgeList.push({ description: "Orbs Apprentice", iconSrc: "https://cdn.discordapp.com/badge-icons/83d8a1eb09a8d64e59233eec5d4d5c2d.png", position: 0, props: { style } });
}
badges.push(...badgeList);
@@ -1887,6 +2224,15 @@ export default definePlugin({
(IconUtils as any).getUserAvatarURL = this._origGetUserAvatarURL;
this._origGetUserAvatarURL = null;
}
// Clean up GuildMemberStore hook
try {
const GMS = (Vencord as any).Webpack?.findByProps?.("getMember", "getMembers", "getMemberIds");
if (GMS?._cp_member_hook) {
if (GMS._cp_orig_getMember) GMS.getMember = GMS._cp_orig_getMember;
delete GMS._cp_member_hook;
delete GMS._cp_orig_getMember;
}
} catch { }
// Nettoyer le patch avatarDecoration
try {
const myUser = UserStore.getCurrentUser() as any;
@@ -1901,5 +2247,3 @@ export default definePlugin({
return <Button onClick={() => openModal(props => <CustomProfileModal rootProps={props} />)}>Open Custom Profile</Button>;
},
});
@@ -56,21 +56,21 @@ export const ThemeCard: React.FC<ThemeCardProps> = ({ theme, enabledThemeLinks,
const isEnabled = enabledThemeLinks.includes(themeUrl);
if (isEnabled) {
// Remove from enabledThemeLinks (ThemeLibrary source of truth)
const nextEnabled = enabledThemeLinks.filter(link => link !== themeUrl);
setEnabledThemeLinks(nextEnabled);
Settings.enabledThemeLinks = nextEnabled;
// Also remove from themeLinks to keep both arrays in sync and avoid
// the theme re-appearing through the Online Themes tab.
Settings.themeLinks = Settings.themeLinks.filter(link => link !== themeUrl);
return;
}
// Add to enabledThemeLinks only — Themes.ts merges both arrays automatically.
// We intentionally do NOT add to themeLinks here to avoid duplicates.
const nextEnabled = [...enabledThemeLinks, themeUrl];
setEnabledThemeLinks(nextEnabled);
Settings.enabledThemeLinks = nextEnabled;
if (!Settings.themeLinks.includes(themeUrl)) {
Settings.themeLinks = [...Settings.themeLinks, themeUrl];
}
// If user enabled a theme from library, online themes must be active globally.
Settings.enableOnlineThemes = true;
};
const handleThemeAttributesCheck = () => {