/* * Vencord, a modification for Discord's desktop app * Copyright (c) 2022 Vendicated and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { ImageIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { openImageModal } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; import { GuildMemberStore, IconUtils, Menu } from "@webpack/common"; import type { Channel, Guild, User } from "discord-types/general"; interface UserContextProps { channel: Channel; guildId?: string; user: User; } interface GuildContextProps { guild?: Guild; } interface GroupDMContextProps { channel: Channel; } const settings = definePluginSettings({ format: { type: OptionType.SELECT, description: "Choose the image format to use for non animated images. Animated images will always use .gif", options: [ { label: "webp", value: "webp", default: true }, { label: "png", value: "png", }, { label: "jpg", value: "jpg", } ] }, imgSize: { type: OptionType.SELECT, description: "The image size to use", options: ["128", "256", "512", "1024", "2048", "4096"].map(n => ({ label: n, value: n, default: n === "1024" })) } }); function openImage(url: string) { const format = url.startsWith("/") ? "png" : settings.store.format; const u = new URL(url, window.location.href); u.searchParams.set("size", settings.store.imgSize); u.pathname = u.pathname.replace(/\.(png|jpe?g|webp)$/, `.${format}`); url = u.toString(); u.searchParams.set("size", "4096"); const originalUrl = u.toString(); openImageModal(url, { original: originalUrl, height: 256 }); } const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => { if (!user) return; const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null; children.splice(-1, 0, ( openImage(IconUtils.getUserAvatarURL(user, true))} icon={ImageIcon} /> {memberAvatar && ( openImage(IconUtils.getGuildMemberAvatarURLSimple({ userId: user.id, avatar: memberAvatar, guildId: guildId!, canAnimate: true }))} icon={ImageIcon} /> )} )); }; const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => { if (!guild) return; const { id, icon, banner } = guild; if (!banner && !icon) return; children.splice(-1, 0, ( {icon ? ( openImage(IconUtils.getGuildIconURL({ id, icon, canAnimate: true })!) } icon={ImageIcon} /> ) : null} {banner ? ( openImage(IconUtils.getGuildBannerURL(guild, true)!) } icon={ImageIcon} /> ) : null} )); }; const GroupDMContext: NavContextMenuPatchCallback = (children, { channel }: GroupDMContextProps) => { if (!channel) return; children.splice(-1, 0, ( openImage(IconUtils.getChannelIconURL(channel)!) } icon={ImageIcon} /> )); }; export default definePlugin({ name: "ViewIcons", authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz, Devs.nyx], description: "Makes avatars and banners in user profiles clickable, adds View Icon/Banner entries in the user, server and group channel context menu.", tags: ["ImageUtilities"], settings, openImage, contextMenus: { "user-context": UserContext, "guild-context": GuildContext, "gdm-context": GroupDMContext }, patches: [ // Profiles Modal pfp { find: "User Profile Modal - Context Menu", replacement: { match: /\{src:(\i)(?=,avatarDecoration)/, replace: "{src:$1,onClick:()=>$self.openImage($1)" } }, // Banners { find: ".NITRO_BANNER,", replacement: { // style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl, match: /style:\{(?=backgroundImage:(null!=\i)\?"url\("\.concat\((\i),)/, replace: // onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0, 'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,' } }, // User DMs "User Profile" popup in the right { find: ".avatarPositionPanel", replacement: { match: /(?<=avatarWrapperNonUserBot.{0,50})onClick:(\i\|\|\i)\?void 0(?<=,avatarSrc:(\i).+?)/, replace: "style:($1)?{cursor:\"pointer\"}:{},onClick:$1?()=>{$self.openImage($2)}" } }, // Group DMs top small & large icon { find: ".recipients.length>=2", all: true, replacement: { match: /null==\i\.icon\?.+?src:(\(0,\i\.getChannelIconURL\).+?\))(?=[,}])/, replace: (m, iconUrl) => `${m},onClick:()=>$self.openImage(${iconUrl})` } }, // User DMs top small icon { find: ".cursorPointer:null,children", replacement: { match: /.Avatar,.+?src:(.+?\))(?=[,}])/, replace: (m, avatarUrl) => `${m},onClick:()=>$self.openImage(${avatarUrl})` } }, // User Dms top large icon { find: 'experimentLocation:"empty_messages"', replacement: { match: /.Avatar,.+?src:(.+?\))(?=[,}])/, replace: (m, avatarUrl) => `${m},onClick:()=>$self.openImage(${avatarUrl})` } } ] });