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
1import { NavContextMenuPatchCallback } from "@api/ContextMenu";
2import { definePluginSettings } from "@api/Settings";
3import { ImageIcon } from "@components/Icons";
4import { Devs } from "@utils/constants";
5import { openImageModal } from "@utils/discord";
6import definePlugin, { OptionType } from "@utils/types";
7import type { Channel, Guild, User } from "@vencord/discord-types";
8import { GuildMemberStore, IconUtils, Menu } from "@webpack/common";
9
10
11interface UserContextProps {
12 channel: Channel;
13 guildId?: string;
14 user: User;
15}
16
17interface GuildContextProps {
18 guild?: Guild;
19}
20
21interface GroupDMContextProps {
22 channel: Channel;
23}
24
25const 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: true
34 },
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
53const openAvatar = (url: string) => openImage(url, 512, 512);
54const openBanner = (url: string) => openImage(url, 1024);
55
56function 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 height
77 });
78}
79
80const 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.MenuItem
87 id="view-avatar"
88 label="View Avatar"
89 action={() => openAvatar(IconUtils.getUserAvatarURL(user, true))}
90 icon={ImageIcon}
91 />
92 {memberAvatar && (
93 <Menu.MenuItem
94 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: true
101 }))}
102 icon={ImageIcon}
103 />
104 )}
105 </Menu.MenuGroup>
106 ));
107};
108
109const 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.MenuItem
119 id="view-icon"
120 label="View Icon"
121 action={() =>
122 openAvatar(IconUtils.getGuildIconURL({
123 id,
124 icon,
125 canAnimate: true
126 })!)
127 }
128 icon={ImageIcon}
129 />
130 ) : null}
131 {banner ? (
132 <Menu.MenuItem
133 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
145const GroupDMContext: NavContextMenuPatchCallback = (children, { channel }: GroupDMContextProps) => {
146 if (!channel) return;
147
148 children.splice(-1, 0, (
149 <Menu.MenuGroup>
150 <Menu.MenuItem
151 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
162export 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": GroupDMContext
179 },
180
181 patches: [
182 // Avatar component used in User DMs "User Profile" popup in the right and User Profile Modal pfp
183 {
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 // Banners
191 {
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 icon
199 {
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 bar
204 replace: (m, iconUrl) => `${m},onClick:()=>arguments[0]?.size!=="SIZE_48"&&$self.openAvatar(${iconUrl})`
205 }
206 },
207 // User DMs top small icon
208 {
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 icon
216 {
217 find: ".EMPTY_GROUP_DM)",
218 replacement: {
219 match: /(?<=SIZE_80,)(?=src:(.+?\))[,}])/,
220 replace: (_, avatarUrl) => `onClick:()=>$self.openAvatar(${avatarUrl}),`
221 }
222 }
223 ]
224});
225