diff --git a/src/core/plugins/badges/index.tsx b/src/core/plugins/badges/index.tsx new file mode 100644 index 00000000..a11e8eec --- /dev/null +++ b/src/core/plugins/badges/index.tsx @@ -0,0 +1,60 @@ +import { addJSXCallback } from "@lib/api/jsx"; +import { after } from "@lib/api/patcher"; +import { findByName } from "@metro"; +import { FC, useEffect, useState } from "react"; +import { ImageSourcePropType, ViewStyle } from "react-native"; + +import { defineCorePlugin } from ".."; + +interface BunnyBadge { + label: string; + url: string; +} + +const useBadgesModule = findByName("useBadges", false); + +export default defineCorePlugin({ + manifest: { + id: "bunny.badges", + name: "Badges", + version: "1.0.0", + description: "Adds badges to user's profile", + authors: [{ name: "pylixonly" }] + }, + start() { + const propHolder = {} as Record; + const badgeCache = {} as Record; + + addJSXCallback("RenderedBadge", (_, ret) => { + if (ret.props.id.match(/bunny-\d+-\d+/)) { + Object.assign(ret.props, propHolder[ret.props.id]); + } + }); + + after("default", useBadgesModule, ([user], r) => { + const [badges, setBadges] = useState(user ? badgeCache[user.userId] ??= [] : []); + + useEffect(() => { + if (user) { + fetch(`https://raw.githubusercontent.com/pyoncord/badges/refs/heads/main/${user.userId}.json`) + .then(r => r.json()) + .then(badges => setBadges(badgeCache[user.userId] = badges)); + } + }, [user]); + + badges.forEach((badges, i) => { + propHolder[`bunny-${user.userId}-${i}`] = { + source: { uri: badges.url }, + id: `bunny-${i}`, + label: badges.label + }; + + r.push({ + id: `bunny-${user.userId}-${i}`, + description: badges.label, + icon: "2ba85e8026a8614b640c2837bcdfe21b", + }); + }); + }); + } +}); diff --git a/src/core/plugins/index.ts b/src/core/plugins/index.ts index e1e4bd9a..4f7a3056 100644 --- a/src/core/plugins/index.ts +++ b/src/core/plugins/index.ts @@ -7,7 +7,8 @@ interface CorePlugin { // Called from @lib/plugins export const getCorePlugins = (): Record => ({ - "bunny.quickinstall": require("./quickinstall") + "bunny.quickinstall": require("./quickinstall"), + "bunny.badges": require("./badges") }); /** diff --git a/src/core/plugins/quickinstall/index.ts b/src/core/plugins/quickinstall/index.ts index f56336a8..51f5e94e 100644 --- a/src/core/plugins/quickinstall/index.ts +++ b/src/core/plugins/quickinstall/index.ts @@ -10,7 +10,7 @@ export default defineCorePlugin({ name: "QuickInstall", version: "1.0.0", description: "Quickly install Vendetta plugins and themes", - authors: ["pyoncord"] + authors: [{ name: "Vendetta Team" }] }, start() { patches = [patchForumPost(), patchUrl()]; diff --git a/src/index.ts b/src/index.ts index 8ccc390b..a0f569db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { VdPluginManager } from "@core/vendetta/plugins"; import { patchCommands } from "@lib/api/commands"; import { patchLogHook } from "@lib/api/debug"; import { injectFluxInterceptor } from "@lib/api/flux"; +import { patchJSX } from "@lib/api/jsx"; import { removeFile, writeFile } from "@lib/api/native/fs"; import { isPyonLoader, isThemeSupported } from "@lib/api/native/loader"; import { FileManager } from "@lib/api/native/modules"; @@ -42,6 +43,7 @@ export default async () => { patchLogHook(), patchCommands(), patchChatBackground(), + patchJSX(), initVendettaObject(), initFetchI18nStrings(), initSettings(), diff --git a/src/lib/api/jsx/index.ts b/src/lib/api/jsx/index.ts new file mode 100644 index 00000000..8d563efc --- /dev/null +++ b/src/lib/api/jsx/index.ts @@ -0,0 +1,40 @@ +import { after } from "@lib/api/patcher"; +import { findByPropsLazy } from "@metro"; + +type Callback = (Component: any, ret: JSX.Element) => void; +const callbacks = new Map(); + +const jsxRuntime = findByPropsLazy("jsx", "jsxs"); + +export function addJSXCallback(Component: string, callback: Callback) { + if (!callbacks.has(Component)) callbacks.set(Component, []); + callbacks.get(Component)!.push(callback); +} + +export function removeJSXCallback(Component: string, callback: Callback) { + if (!callbacks.has(Component)) return; + const cbs = callbacks.get(Component)!; + cbs.splice(cbs.indexOf(callback), 1); + if (cbs.length === 0) callbacks.delete(Component); +} + +/** + * @internal + */ +export function patchJSX() { + // Only a simple name check for now + const callback = ([Component, props]: any[], ret: any) => { + if (typeof Component === "function" && callbacks.has(Component.name)) { + const cbs = callbacks.get(Component.name)!; + for (const cb of cbs) ret = cb(Component, ret); + return ret; + } + }; + + const patches = [ + after("jsx", jsxRuntime, callback), + after("jsxs", jsxRuntime, callback) + ]; + + return () => patches.forEach(unpatch => unpatch()); +}