Plugin
ViewIcons
Makes avatars and banners in user profiles clickable, adds View Icon/Banner entries in the user, server and group channel context menu.
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import { NavContextMenuPatchCallback } from "@api/ContextMenu";8
import { definePluginSettings } from "@api/Settings";9
import { ImageIcon } from "@components/Icons";10
import { Devs } from "@utils/constants";11
import { openImageModal } from "@utils/discord";12
import definePlugin, { OptionType } from "@utils/types";13
import type { Channel, Guild, User } from "@vencord/discord-types";14
import { GuildMemberStore, IconUtils, Menu } from "@webpack/common";15
16
17
interface UserContextProps {18
channel: Channel;19
guildId?: string;20
user: User;21
}22
23
interface GuildContextProps {24
guild?: Guild;25
}26
27
interface GroupDMContextProps {28
channel: Channel;29
}30
31
const settings = definePluginSettings({32
format: {33
type: OptionType.SELECT,34
description: "Choose the image format to use for non animated images. Animated images will always use .gif",35
options: [36
{37
label: "webp",38
value: "webp",39
default: true40
},41
{42
label: "png",43
value: "png",44
},45
{46
label: "jpg",47
value: "jpg",48
}49
]50
},51
imgSize: {52
type: OptionType.SELECT,53
displayName: "Image Size",54
description: "The image size to use",55
options: ["128", "256", "512", "1024", "2048", "4096"].map(n => ({ label: n, value: n, default: n === "1024" }))56
}57
});58
59
const openAvatar = (url: string) => openImage(url, 512, 512);60
const openBanner = (url: string) => openImage(url, 1024);61
62
function openImage(url: string, width: number, height?: number) {63
const u = new URL(url, window.location.href);64
65
const format = url.startsWith("/")66
? "png"67
: u.searchParams.get("animated") === "true"68
? "gif"69
: settings.store.format;70
71
u.searchParams.set("size", settings.store.imgSize);72
u.pathname = u.pathname.replace(/\.(png|jpe?g|webp)$/, `.${format}`);73
url = u.toString();74
75
u.searchParams.set("size", "4096");76
const original = u.toString();77
78
openImageModal({79
url,80
original,81
width,82
height83
});84
}85
86
const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => {87
if (!user) return;88
const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null;89
90
children.splice(-1, 0, (91
<Menu.MenuGroup>92
<Menu.MenuItem93
id="view-avatar"94
label="View Avatar"95
action={() => openAvatar(IconUtils.getUserAvatarURL(user, true))}96
icon={ImageIcon}97
/>98
{memberAvatar && (99
<Menu.MenuItem100
id="view-server-avatar"101
label="View Server Avatar"102
action={() => openAvatar(IconUtils.getGuildMemberAvatarURLSimple({103
userId: user.id,104
avatar: memberAvatar,105
guildId: guildId!,106
canAnimate: true107
}))}108
icon={ImageIcon}109
/>110
)}111
</Menu.MenuGroup>112
));113
};114
115
const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => {116
if (!guild) return;117
118
const { id, icon, banner } = guild;119
if (!banner && !icon) return;120
121
children.splice(-1, 0, (122
<Menu.MenuGroup>123
{icon ? (124
<Menu.MenuItem125
id="view-icon"126
label="View Icon"127
action={() =>128
openAvatar(IconUtils.getGuildIconURL({129
id,130
icon,131
canAnimate: true132
})!)133
}134
icon={ImageIcon}135
/>136
) : null}137
{banner ? (138
<Menu.MenuItem139
id="view-banner"140
label="View Banner"141
action={() =>142
openBanner(IconUtils.getGuildBannerURL(guild, true)!)143
}144
icon={ImageIcon}145
/>146
) : null}147
</Menu.MenuGroup>148
));149
};150
151
const GroupDMContext: NavContextMenuPatchCallback = (children, { channel }: GroupDMContextProps) => {152
if (!channel) return;153
154
children.splice(-1, 0, (155
<Menu.MenuGroup>156
<Menu.MenuItem157
id="view-group-channel-icon"158
label="View Icon"159
action={() =>160
openAvatar(IconUtils.getChannelIconURL(channel)!)161
}162
icon={ImageIcon}163
/>164
</Menu.MenuGroup>165
));166
};167
168
export default definePlugin({169
name: "ViewIcons",170
authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz, Devs.nyx],171
description: "Makes avatars and banners in user profiles clickable, adds View Icon/Banner entries in the user, server and group channel context menu.",172
tags: ["Media", "Servers", "Appearance"],173
searchTerms: ["ImageUtilities"],174
dependencies: ["DynamicImageModalAPI"],175
176
settings,177
178
openAvatar,179
openBanner,180
181
contextMenus: {182
"user-context": UserContext,183
"guild-context": GuildContext,184
"gdm-context": GroupDMContext185
},186
187
patches: [188
// Avatar component used in User DMs "User Profile" popup in the right and User Profile Modal pfp189
{190
find: "return{avatarProps:{",191
replacement: {192
match: /(?<=avatarProps:(\i),eventHandlers:(\i).{0,50}?)return null==/,193
replace: 039;Object.assign($2,{style:{cursor:"pointer"},onClick:()=>$self.openAvatar($1.src)});$&039;,194
}195
},196
// Banners197
{198
find: 039;backgroundColor:"COMPLETE"039;,199
replacement: {200
match: /(overflow:"visible",.{0,125}?!1\),)style:{(?=.+?backgroundImage:null!=(\i)\?`url\(\$\{\2\}\))/,201
replace: (_, rest, bannerSrc) => `${rest}onClick:()=>${bannerSrc}!=null&&$self.openBanner(${bannerSrc}),style:{cursor:${bannerSrc}!=null?"pointer":void 0,`202
}203
},204
// Group DMs top small & large icon205
{206
find: 039;["aria-hidden"],"aria-label":039;,207
replacement: {208
match: /null==\i\.icon\?.+?src:(\(0,\i\.\i\).+?\))(?=[,}])/,209
// We have to check that icon is not an unread GDM in the server bar210
replace: (m, iconUrl) => `${m},onClick:()=>arguments[0]?.size!=="SIZE_48"&&$self.openAvatar(${iconUrl})`211
}212
},213
// User DMs top small icon214
{215
find: ".channel.getRecipientId(),",216
replacement: {217
match: /(?=,src:(\i.getAvatarURL\(.+?[)]))/,218
replace: (_, avatarUrl) => `,onClick:()=>$self.openAvatar(${avatarUrl})`219
}220
},221
// User Dms top large icon222
{223
find: ".EMPTY_GROUP_DM)",224
replacement: {225
match: /(?<=SIZE_80,)(?=src:(.+?\))[,}])/,226
replace: (_, avatarUrl) => `onClick:()=>$self.openAvatar(${avatarUrl}),`227
}228
}229
]230
});231