diff --git a/src/plugins/bannersEverywhere/README.md b/src/plugins/bannersEverywhere/README.md new file mode 100644 index 000000000..b868a6a9a --- /dev/null +++ b/src/plugins/bannersEverywhere/README.md @@ -0,0 +1,5 @@ +# BannersEverywhere + +Displays nitro and USRBG banners as a background in the member list + +![](https://github.com/Vendicated/Vencord/assets/44179559/d8a3a2f2-8491-4a4f-b43e-22fdf46622e5) diff --git a/src/plugins/bannersEverywhere/index.css b/src/plugins/bannersEverywhere/index.css new file mode 100644 index 000000000..269baf4ad --- /dev/null +++ b/src/plugins/bannersEverywhere/index.css @@ -0,0 +1,13 @@ +/* stylelint-disable property-no-vendor-prefix */ +.vc-banners-everywhere-memberlist { + opacity: 0.8; + -webkit-mask-image: linear-gradient(to right, transparent 20%, #fff); + mask-image: linear-gradient(to right, transparent 20%, #fff); + height: 100%; + width: 100%; + background-size: cover; + background-position: center; + position: absolute; + border-radius: 4px; + cursor: pointer; +} \ No newline at end of file diff --git a/src/plugins/bannersEverywhere/index.tsx b/src/plugins/bannersEverywhere/index.tsx new file mode 100644 index 000000000..b23026b73 --- /dev/null +++ b/src/plugins/bannersEverywhere/index.tsx @@ -0,0 +1,117 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./index.css"; + +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { fetchUserProfile } from "@utils/discord"; +import { Queue } from "@utils/Queue"; +import { useAwaiter } from "@utils/react"; +import definePlugin, { OptionType } from "@utils/types"; +import { useEffect, UserProfileStore, useStateFromStores } from "@webpack/common"; +import { User } from "discord-types/general"; + +const settings = definePluginSettings({ + animate: { + description: "Animate banners", + type: OptionType.BOOLEAN, + default: false + }, +}); + +const discordQueue = new Queue(); +const usrbgQueue = new Queue(); + + +const useFetchMemberProfile = (userId: string): string => { + const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(userId)); + + const usrbgUrl = (Vencord.Plugins.plugins.USRBG as any)?.getImageUrl(userId); + + useEffect(() => { + if (usrbgUrl) return; + let cancel = false; + + discordQueue.push(() => { + if (cancel) return Promise.resolve(void 0); + return fetchUserProfile(userId).finally(async () => { + await new Promise(resolve => setTimeout(resolve, 1000)); + }); + }); + + return () => { cancel = true; }; + }, []); + + if (usrbgUrl) return usrbgUrl; + + if (!profile?.banner) return ""; + const extension = settings.store.animate && profile.banner.startsWith("a_") + ? ".gif" + : ".png"; + return `https://cdn.discordapp.com/banners/${userId}/${profile.banner}${extension}`; +}; +export default definePlugin({ + name: "BannersEverywhere", + description: "Displays banners in the member list ", + authors: [Devs.ImLvna, Devs.AutumnVN], + settings, + patches: [ + { + find: ".Messages.GUILD_OWNER,", + replacement: + { + // We add the banner as a property while we can still access the user id + match: /verified:(\i).isVerifiedBot.*?name:null.*?(?=avatar:)/, + replace: "$&banner:$self.memberListBanner({user: $1}),", + }, + }, + { + find: "role:\"listitem\",innerRef", + replacement: + { + // We cant access the user id here, so we take the banner property we set earlier + match: /let{avatar:\i.*?focusProps:\i.*?=(\i).*?children:\[/, + replace: "$&$1.banner," + } + } + + ], + + memberListBanner: ErrorBoundary.wrap(({ user }: { user: User; }) => { + const url = useFetchMemberProfile(user.id); + + const [shouldShow] = useAwaiter(async () => { + // This will get re-run when the url changes + if (!url || url === "") return false; + if (!settings.store.animate) { + // Discord cdn can return both png and gif, useFetchMemberProfile gives it respectively + if (url!.includes("cdn.discordapp.com")) return true; + + // HEAD request to check if the image is a png + return await new Promise(resolve => { + usrbgQueue.push(() => fetch(url!.replace(".gif", ".png"), { method: "HEAD" }).then(async res => { + console.log(res); + await new Promise(resolve => setTimeout(resolve, 1000)); + resolve(res.ok && res.headers.get("content-type")?.startsWith("image/png")); + return; + })); + }); + } + return true; + }, { fallbackValue: false, deps: [url] }); + + if (!shouldShow) return null; + return ( + + ); + }, { noop: true }), + + + + +});