Plugin
Decor
Create and use your own custom avatar decorations, or pick your favorite from the presets.
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import "./ui/styles.css";8
9
import ErrorBoundary from "@components/ErrorBoundary";10
import { Devs } from "@utils/constants";11
import definePlugin from "@utils/types";12
import { UserStore } from "@webpack/common";13
14
import { CDN_URL, RAW_SKU_ID, SKU_ID } from "./lib/constants";15
import { useAuthorizationStore } from "./lib/stores/AuthorizationStore";16
import { useCurrentUserDecorationsStore } from "./lib/stores/CurrentUserDecorationsStore";17
import { useUserDecorAvatarDecoration, useUsersDecorationsStore } from "./lib/stores/UsersDecorationsStore";18
import { settings } from "./settings";19
import { setAvatarDecorationModalPreview, setDecorationGridDecoration, setDecorationGridItem } from "./ui/components";20
import DecorSection from "./ui/components/DecorSection";21
22
export interface AvatarDecoration {23
asset: string;24
skuId: string;25
}26
27
export default definePlugin({28
name: "Decor",29
description: "Create and use your own custom avatar decorations, or pick your favorite from the presets.",30
tags: ["Appearance", "Customisation"],31
authors: [Devs.FieryFlames],32
patches: [33
// Patch MediaResolver to return correct URL for Decor avatar decorations34
{35
find: "getAvatarDecorationURL:",36
replacement: {37
match: /(?<=function \i\(\i\){)(?=let{avatarDecoration)/,38
replace: "const vcDecorDecoration=$self.getDecorAvatarDecorationURL(arguments[0]);if(vcDecorDecoration)return vcDecorDecoration;"39
}40
},41
// Patch profile customization settings to include Decor section42
{43
find: "DefaultCustomizationSections",44
replacement: {45
match: /(?<=#{intl::USER_SETTINGS_AVATAR_DECORATION}\)},"decoration"\),)/,46
replace: "$self.DecorSection(),"47
}48
},49
// Decoration modal module50
{51
find: "80,onlyAnimateOnHoverOrFocus:!",52
replacement: [53
{54
match: /(?<==)\i=>{let{children.{20,200}isSelected:\i.{0,5}\}=\i/,55
replace: "$self.DecorationGridItem=$&",56
},57
{58
match: /(?<==)\i=>{let{user:\i,avatarDecoration/,59
replace: "$self.DecorationGridDecoration=$&",60
},61
// Remove NEW label from decor avatar decorations62
{63
match: /(?<=\i\.PURCHASE)(?=,)(?<=avatarDecoration:(\i).+?)/,64
replace: "||$1.skuId===$self.SKU_ID"65
}66
]67
},68
{69
find: "isAvatarDecorationAnimating:",70
group: true,71
replacement: [72
// Add Decor avatar decoration hook to avatar decoration hook73
{74
match: /(?<=\.avatarDecoration,guildId:\i\}\)\),)(?<=user:(\i).+?)/,75
replace: "vcDecorAvatarDecoration=$self.useUserDecorAvatarDecoration($1),"76
},77
// Use added hook78
{79
match: /(?<={avatarDecoration:).{1,20}?(?=,)(?<=avatarDecorationOverride:(\i).+?)/,80
replace: "$1??vcDecorAvatarDecoration??($&)"81
},82
// Make memo depend on added hook83
{84
match: /(?<=size:\i}\),\[)/,85
replace: "vcDecorAvatarDecoration,"86
}87
]88
},89
// Current user area, at bottom of channels/dm list90
{91
find: ".DISPLAY_NAME_STYLES_COACHMARK)",92
replacement: [93
// Use Decor avatar decoration hook94
{95
match: /(?<=\i\)\({avatarDecoration:)\i(?=,)(?<=currentUser:(\i).+?)/,96
replace: "$self.useUserDecorAvatarDecoration($1)??$&"97
}98
]99
},100
...[101
"#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}", class="ts-cmt">// Messages102
"#{intl::COLLECTIBLES_NAMEPLATE_PREVIEW_A11Y}", class="ts-cmt">// Nameplate preview103
"#{intl::COLLECTIBLES_PROFILE_PREVIEW_A11Y}", class="ts-cmt">// Avatar preview104
].map(find => ({105
find,106
replacement: {107
match: /(?<=userValue:)((\i(?:\.author)?)\?\.avatarDecoration)/,108
replace: "$self.useUserDecorAvatarDecoration($2)??$1"109
}110
})),111
// Patch avatar decoration preview to display Decor avatar decorations as if they are purchased112
{113
find: "#{intl::PREMIUM_UPSELL_PROFILE_AVATAR_DECO_INLINE_UPSELL_DESCRIPTION}",114
replacement: [115
{116
match: /(?<==)\i=>{let{user:\i,guildId:\i,avatarDecoration:/,117
replace: "$self.AvatarDecorationModalPreview=$&"118
}119
]120
}121
],122
settings,123
124
flux: {125
CONNECTION_OPEN: () => {126
useAuthorizationStore.getState().init();127
useCurrentUserDecorationsStore.getState().clear();128
useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true);129
},130
USER_PROFILE_MODAL_OPEN: data => {131
useUsersDecorationsStore.getState().fetch(data.userId, true);132
},133
},134
135
set DecorationGridItem(e: any) {136
setDecorationGridItem(e);137
},138
139
set DecorationGridDecoration(e: any) {140
setDecorationGridDecoration(e);141
},142
143
set AvatarDecorationModalPreview(e: any) {144
setAvatarDecorationModalPreview(e);145
},146
147
SKU_ID,148
RAW_SKU_ID,149
150
useUserDecorAvatarDecoration,151
152
async start() {153
useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true);154
},155
156
getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) {157
// Only Decor avatar decorations have this SKU ID158
if (avatarDecoration?.skuId === SKU_ID) {159
const parts = avatarDecoration.asset.split("_");160
// Remove a_ prefix if it's animated and animation is disabled161
if (avatarDecoration.asset.startsWith("a_") && !canAnimate) parts.shift();162
return `${CDN_URL}/${parts.join("_")}.png`;163
} else if (avatarDecoration?.skuId === RAW_SKU_ID) {164
return avatarDecoration.asset;165
}166
},167
168
DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })169
});170