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