Plugin

ViewIcons

Makes avatars and banners in user profiles clickable, adds View Icon/Banner entries in the user, server and group channel context menu.

Media Servers Appearance
index.tsx
Download

Source

src/plugins/viewIcons/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import { NavContextMenuPatchCallback } from "@api/ContextMenu";
8import { definePluginSettings } from "@api/Settings";
9import { ImageIcon } from "@components/Icons";
10import { Devs } from "@utils/constants";
11import { openImageModal } from "@utils/discord";
12import definePlugin, { OptionType } from "@utils/types";
13import type { Channel, Guild, User } from "@vencord/discord-types";
14import { GuildMemberStore, IconUtils, Menu } from "@webpack/common";
15
16
17interface UserContextProps {
18 channel: Channel;
19 guildId?: string;
20 user: User;
21}
22
23interface GuildContextProps {
24 guild?: Guild;
25}
26
27interface GroupDMContextProps {
28 channel: Channel;
29}
30
31const 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: true
40 },
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
59const openAvatar = (url: string) => openImage(url, 512, 512);
60const openBanner = (url: string) => openImage(url, 1024);
61
62function 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 height
83 });
84}
85
86const 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.MenuItem
93 id="view-avatar"
94 label="View Avatar"
95 action={() => openAvatar(IconUtils.getUserAvatarURL(user, true))}
96 icon={ImageIcon}
97 />
98 {memberAvatar && (
99 <Menu.MenuItem
100 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: true
107 }))}
108 icon={ImageIcon}
109 />
110 )}
111 </Menu.MenuGroup>
112 ));
113};
114
115const 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.MenuItem
125 id="view-icon"
126 label="View Icon"
127 action={() =>
128 openAvatar(IconUtils.getGuildIconURL({
129 id,
130 icon,
131 canAnimate: true
132 })!)
133 }
134 icon={ImageIcon}
135 />
136 ) : null}
137 {banner ? (
138 <Menu.MenuItem
139 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
151const GroupDMContext: NavContextMenuPatchCallback = (children, { channel }: GroupDMContextProps) => {
152 if (!channel) return;
153
154 children.splice(-1, 0, (
155 <Menu.MenuGroup>
156 <Menu.MenuItem
157 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
168export 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": GroupDMContext
185 },
186
187 patches: [
188 // Avatar component used in User DMs "User Profile" popup in the right and User Profile Modal pfp
189 {
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 // Banners
197 {
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 icon
205 {
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 bar
210 replace: (m, iconUrl) => `${m},onClick:()=>arguments[0]?.size!=="SIZE_48"&&$self.openAvatar(${iconUrl})`
211 }
212 },
213 // User DMs top small icon
214 {
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 icon
222 {
223 find: ".EMPTY_GROUP_DM)",
224 replacement: {
225 match: /(?<=SIZE_80,)(?=src:(.+?\))[,}])/,
226 replace: (_, avatarUrl) => `onClick:()=>$self.openAvatar(${avatarUrl}),`
227 }
228 }
229 ]
230});
231