From e6c05a98d85baed189ed66240993fc8d94d39e94 Mon Sep 17 00:00:00 2001 From: Ally Founder Date: Tue, 11 Aug 2026 08:57:18 -0400 Subject: [PATCH] ServerDrawer: diagnostic pass - match original's infra exactly --- .../src/components/ContextMenuModal.tsx | 32 +- .../server-drawer/src/components/DmTile.tsx | 31 +- .../src/components/FolderItem.tsx | 18 +- .../src/components/GuildIcon.tsx | 19 +- .../src/components/GuildItem.tsx | 48 +-- .../src/components/ServerDrawerSheet.tsx | 58 ++- plugins/server-drawer/src/index.ts | 85 +---- .../src/lib/createElementIntercept.ts | 359 ++---------------- .../src/patches/contentPatch.tsx | 61 +-- .../src/patches/hideGuildsBar.tsx | 139 ++----- plugins/server-drawer/src/utils/theme.ts | 20 +- 11 files changed, 169 insertions(+), 701 deletions(-) diff --git a/plugins/server-drawer/src/components/ContextMenuModal.tsx b/plugins/server-drawer/src/components/ContextMenuModal.tsx index 891e590..2d19795 100644 --- a/plugins/server-drawer/src/components/ContextMenuModal.tsx +++ b/plugins/server-drawer/src/components/ContextMenuModal.tsx @@ -1,20 +1,17 @@ import React from "react"; import { View, Text, Pressable, Image, Modal, StyleSheet, Dimensions, Animated } from "react-native"; import { semanticColors } from "@vendetta/ui"; -import { resolveSemanticColor } from "../lib/color"; -import { lazy } from "../lib/lazy"; -import { rawFindByValidProps } from "../lib/rawFind"; -import { getHaptic } from "../lib/commonModules"; +import { findByProps, findByStoreName } from "@vendetta/metro"; -// getHaptic comes from lib/commonModules.ts - see that file for the decoy-module writeup. -// TextStyleSheet is object-shaped (a style lookup table), so it needs the same shape-validated -// lookup rather than a plain "does this property exist" check. -const getTextStyleSheet = lazy(() => rawFindByValidProps({ - TextStyleSheet: (v) => v?.["text-md/bold"] !== undefined, -})?.TextStyleSheet); +const TextStyleSheet = findByProps("TextStyleSheet")?.TextStyleSheet; +const ThemeStore = findByStoreName("ThemeStore"); +const colorModule = findByProps("colors", "unsafe_rawColors"); +const colorResolver = colorModule?.internal ?? colorModule?.meta; +const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes"); -function resolve(token: any, fallback: string): string { - return resolveSemanticColor(token) ?? fallback; +function resolve(color: any): string { + const theme = ThemeStore?.theme; + return (color && colorResolver?.resolveSemanticColor(theme, color)) || "#000000"; } const EDGE = 12; @@ -50,9 +47,6 @@ export function ContextMenuModal({ anchorY: number; onClose: () => void; }) { - const TextStyleSheet = getTextStyleSheet(); - const Haptic = getHaptic(); - const { width: winW, height: winH } = Dimensions.get("window"); const titleH = title ? PAD + 20 + DIVIDER_H : 0; @@ -65,10 +59,10 @@ export function ContextMenuModal({ if (top + menuH > winH - EDGE) top = anchorY - menuH - 10; if (top < EDGE) top = EDGE; - const bgContainer = resolve(semanticColors?.BACKGROUND_SURFACE_HIGHEST, "#1e1f22"); - const borderColor = resolve(semanticColors?.BORDER_SUBTLE, "#3f4147"); - const textColor = resolve(semanticColors?.TEXT_STRONG, "#f2f3f5"); - const pressedColor = resolve(semanticColors?.BACKGROUND_MOD_SUBTLE, "rgba(255,255,255,0.06)"); + const bgContainer = resolve(semanticColors?.BACKGROUND_SURFACE_HIGHEST); + const borderColor = resolve(semanticColors?.BORDER_SUBTLE); + const textColor = resolve(semanticColors?.TEXT_STRONG); + const pressedColor = resolve(semanticColors?.BACKGROUND_MOD_SUBTLE); const backdropOpacity = React.useRef(new Animated.Value(0)).current; const menuScale = React.useRef(new Animated.Value(0.5)).current; diff --git a/plugins/server-drawer/src/components/DmTile.tsx b/plugins/server-drawer/src/components/DmTile.tsx index 7391599..f33974f 100644 --- a/plugins/server-drawer/src/components/DmTile.tsx +++ b/plugins/server-drawer/src/components/DmTile.tsx @@ -1,42 +1,27 @@ import React from "react"; import { View, Pressable, Image, StyleSheet } from "react-native"; import { getAssetIDByName } from "@vendetta/ui/assets"; -import { lazy } from "../lib/lazy"; -import { rawFindByFunctionProps, rawFindByName } from "../lib/rawFind"; -import { getFlux, getHaptic, getColorModule } from "../lib/commonModules"; +import { findByProps, findByName } from "@vendetta/metro"; const ICON = 48; const ChatIcon = getAssetIDByName("ChatIcon"); - -// getFlux/getHaptic/getColorModule come from lib/commonModules.ts - see that file for the -// decoy-module writeup. The rest switch from eager, module-load-time findByProps/findByName (the -// caching finders, permanently wrong if they run before the target module registers) to lazy + -// raw variants. NavContext is findByProps("getGuildId") - a navigation-context helper, NOT the -// real guild data store (that one is reached via findByStoreName elsewhere in this plugin) - -// named accordingly here to avoid confusing the two. -const getChannelActions = lazy(() => rawFindByFunctionProps("selectPrivateChannel")); -const getSelectedChannelStore = lazy(() => rawFindByName("SelectedChannelStore")); -const getNavContext = lazy(() => rawFindByFunctionProps("getGuildId")); +const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes"); +const ChannelActions = findByProps("selectPrivateChannel"); +const SelectedChannelStore = findByName("SelectedChannelStore"); +const Flux = findByProps("useStateFromStores"); +const NavContext = findByProps("getGuildId"); +const colors = findByProps("colors", "unsafe_rawColors")?.colors; function openDms() { - const Haptic = getHaptic(); Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.SOFT); - const ChannelActions = getChannelActions(); if (ChannelActions?.selectPrivateChannel) { - const lastChannelId = getSelectedChannelStore()?.getLastSelectedChannelId?.(); + const lastChannelId = SelectedChannelStore?.getLastSelectedChannelId?.(); ChannelActions.selectPrivateChannel(lastChannelId); } } -// Was previously a full replacement for the native left server rail (rendered instead of it) - -// moved into the drawer's own grid instead, as the first tile, so DMs live in the same place as -// every other server rather than in a leftover spot from the sidebar this plugin otherwise hides. export default function DmTile() { - const Flux = getFlux(); - const NavContext = getNavContext(); - const colors = getColorModule()?.colors; - const selected = Flux?.useStateFromStores?.( [NavContext], () => NavContext?.getGuildId?.() == null, diff --git a/plugins/server-drawer/src/components/FolderItem.tsx b/plugins/server-drawer/src/components/FolderItem.tsx index 578949f..5d3eb74 100644 --- a/plugins/server-drawer/src/components/FolderItem.tsx +++ b/plugins/server-drawer/src/components/FolderItem.tsx @@ -1,21 +1,16 @@ import React from "react"; import { View, Text, Animated, Pressable, Image, ViewStyle, StyleSheet } from "react-native"; +import { findByProps, findByStoreName } from "@vendetta/metro"; import { getAssetIDByName } from "@vendetta/ui/assets"; import { storage } from "@vendetta/plugin"; import { useProxy } from "@vendetta/storage"; import { useFolderExpanded, GuildNode } from "../utils/theme"; -import { lazy } from "../lib/lazy"; -import { rawFindByFunctionProps, rawFindByStoreName } from "../lib/rawFind"; -import { getFlux } from "../lib/commonModules"; import GuildIcon from "./GuildIcon"; import GuildItem from "./GuildItem"; -// getFlux comes from lib/commonModules.ts - see that file for the decoy-module writeup. -// GuildActions/GuildReadStateStore switch from eager, module-load-time findByProps/findByStoreName -// (the caching finders) to lazy + raw variants for the same reasons established elsewhere in this -// plugin - timing (permanent-negative-cache) for the store, and decoy-avoidance for the function. -const getGuildActions = lazy(() => rawFindByFunctionProps("toggleGuildFolderExpand")); -const getGuildReadStateStore = lazy(() => rawFindByStoreName("GuildReadStateStore")); +const GuildActions = findByProps("toggleGuildFolderExpand"); +const Flux = findByProps("useStateFromStores"); +const GuildReadStateStore = findByStoreName("GuildReadStateStore"); const ICON = 48; const MINI = 16; @@ -35,9 +30,6 @@ function folderColor(color?: number | null): string { } function FolderBadge({ node }: { node: GuildNode }) { - const Flux = getFlux(); - const GuildReadStateStore = getGuildReadStateStore(); - const total = Flux?.useStateFromStores?.( [GuildReadStateStore], () => { @@ -105,7 +97,7 @@ export default function FolderItem({ node, onPick }: { node: GuildNode; onPick: const open = useFolderExpanded(node.id); const toggle = () => { - getGuildActions()?.toggleGuildFolderExpand?.(node.id); + GuildActions?.toggleGuildFolderExpand?.(node.id); }; if (open) { diff --git a/plugins/server-drawer/src/components/GuildIcon.tsx b/plugins/server-drawer/src/components/GuildIcon.tsx index 2d28901..9aaf6af 100644 --- a/plugins/server-drawer/src/components/GuildIcon.tsx +++ b/plugins/server-drawer/src/components/GuildIcon.tsx @@ -1,23 +1,12 @@ import React from "react"; import { View, Text, Image, StyleSheet } from "react-native"; -import { lazy } from "../lib/lazy"; -import { rawFindByStoreName } from "../lib/rawFind"; -import { getFlux, getColorModule } from "../lib/commonModules"; +import { findByStoreName, findByProps } from "@vendetta/metro"; -// getFlux/getColorModule come from lib/commonModules.ts - see that file for the decoy-module -// writeup. GuildStore switches from an eager, module-load-time findByStoreName (the caching -// finder, permanently wrong if it runs before the store module has registered) to lazy + -// rawFindByStoreName for the same timing reason established elsewhere in this plugin. -const getGuildStore = lazy(() => rawFindByStoreName("GuildStore")); +const GuildStore = findByStoreName("GuildStore"); +const colors = findByProps("colors", "unsafe_rawColors")?.colors; export default function GuildIcon({ id, size = 48 }: { id: string; size?: number }) { - const Flux = getFlux(); - const GuildStore = getGuildStore(); - const colors = getColorModule()?.colors; - // Guild data streams in progressively after connect, so a one-shot GuildStore.getGuild(id) - // taken at mount can miss it entirely and never recover - subscribing means the icon fills in - // as soon as its guild finishes loading instead of staying blank for the rest of the session. - const g = Flux?.useStateFromStores?.([GuildStore], () => GuildStore?.getGuild?.(id), [id]) ?? null; + const g = GuildStore?.getGuild(id); if (!g) return null; const rad = size >= 40 ? 16 : 8; if (g.icon) { diff --git a/plugins/server-drawer/src/components/GuildItem.tsx b/plugins/server-drawer/src/components/GuildItem.tsx index 9451d6f..43700e4 100644 --- a/plugins/server-drawer/src/components/GuildItem.tsx +++ b/plugins/server-drawer/src/components/GuildItem.tsx @@ -1,35 +1,22 @@ import React from "react"; import { View, Text, Pressable, Animated, StyleSheet } from "react-native"; +import { find, findByProps, findByStoreName } from "@vendetta/metro"; import { storage } from "@vendetta/plugin"; import { useProxy } from "@vendetta/storage"; -import { lazy } from "../lib/lazy"; -import { rawFind, rawFindByStoreName } from "../lib/rawFind"; -import { getFlux, getHaptic } from "../lib/commonModules"; -import { GuildNode, useTheme } from "../utils/theme"; +import { GuildNode } from "../utils/theme"; import GuildIcon from "./GuildIcon"; import { ContextMenuModal, ContextMenuItem } from "./ContextMenuModal"; const ICON = 48; -const getGuildReadStateStore = lazy(() => rawFindByStoreName("GuildReadStateStore")); -const getGuildStore = lazy(() => rawFindByStoreName("GuildStore")); -const getSortedGuildStore = lazy(() => rawFindByStoreName("SortedGuildStore")); - -// Discord's own per-guild context menu (getGuildsBarGuildMenuItems) is a bare default export with -// no named export - matches the original plugin's own heuristic (a short function whose name -// mentions "guild"), kept as-is since it's the only lead either version has ever had for finding -// it. Rendered through ContextMenuModal rather than Discord's own native menu component - there's -// no accessible way to mount that component standalone outside its usual native modal host - but -// every item/action in it is Discord's real menu, not a custom-built one. -const getMenuItemsFn = lazy(() => - rawFind((m) => m?.default?.name === "getGuildsBarGuildMenuItems")?.default - ?? rawFind((m) => typeof m?.default === "function" && m.default.length <= 2 && /guild/i.test(m.default.name ?? ""))?.default -); +const Flux = findByProps("useStateFromStores"); +const GuildReadStateStore = findByStoreName("GuildReadStateStore"); +const GuildStore = findByStoreName("GuildStore"); +const SortedGuildStore = findByStoreName("SortedGuildStore"); +const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes"); +const colors = findByProps("colors", "unsafe_rawColors")?.colors; function Badge({ guildId }: { guildId: string }) { - const Flux = getFlux(); - const GuildReadStateStore = getGuildReadStateStore(); - const mentionCount = Flux?.useStateFromStores?.( [GuildReadStateStore], () => GuildReadStateStore?.getMentionCount?.(guildId) ?? 0, @@ -65,10 +52,6 @@ function Badge({ guildId }: { guildId: string }) { function GuildLabel({ guildId }: { guildId: string }) { useProxy(storage); - const theme = useTheme(); - - const Flux = getFlux(); - const GuildStore = getGuildStore(); const name = Flux?.useStateFromStores?.( [GuildStore], @@ -79,7 +62,7 @@ function GuildLabel({ guildId }: { guildId: string }) { if (storage.showGuildNames === false || !name) return null; return ( - + {name} ); @@ -101,11 +84,15 @@ export default function GuildItem({ node, onPick }: { node: GuildNode; onPick: ( const showMenu = React.useCallback(() => { const guildId = node.id as string; - const guild = getGuildStore()?.getGuild?.(guildId); - const menuItemsFn = getMenuItemsFn(); - if (!guild || !menuItemsFn) return; + const guild = GuildStore?.getGuild?.(guildId); + const menuItemsFn = find((m) => m?.default?.name === "getGuildsBarGuildMenuItems")?.default + ?? find((m) => typeof m?.default === "function" && m.default.length <= 2 && /guild/i.test(m.default.name ?? ""))?.default; - const treeVersion = getSortedGuildStore()?.getGuildsTree?.()?.version; + if (!guild || !menuItemsFn) { + return; + } + + const treeVersion = SortedGuildStore?.getGuildsTree?.()?.version; const rawItems = menuItemsFn(guildId, treeVersion); if (!rawItems?.length) return; @@ -130,7 +117,6 @@ export default function GuildItem({ node, onPick }: { node: GuildNode; onPick: ( }, [node.id]); const handleLongPress = React.useCallback(() => { - const Haptic = getHaptic(); Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.IMPACT_MEDIUM); showMenu(); }, [showMenu]); diff --git a/plugins/server-drawer/src/components/ServerDrawerSheet.tsx b/plugins/server-drawer/src/components/ServerDrawerSheet.tsx index c6c2ac7..fd0f24e 100644 --- a/plugins/server-drawer/src/components/ServerDrawerSheet.tsx +++ b/plugins/server-drawer/src/components/ServerDrawerSheet.tsx @@ -1,25 +1,29 @@ import React from "react"; import { View, Text, Pressable, Animated, Dimensions, StyleSheet, BackHandler } from "react-native"; +import { find, findByProps, findByStoreName } from "@vendetta/metro"; import { storage } from "@vendetta/plugin"; import { useProxy } from "@vendetta/storage"; -import { lazy } from "../lib/lazy"; -import { rawFind, rawFindByFunctionProps } from "../lib/rawFind"; -import { getFlux, getHaptic, getColorModule } from "../lib/commonModules"; import { GuildNode } from "../utils/theme"; import GuildItem from "./GuildItem"; import FolderItem from "./FolderItem"; import DmTile from "./DmTile"; -// getFlux/getHaptic/getColorModule come from lib/commonModules.ts - see that file for the decoy -// module writeup. These three below are specific to this file only. -const getRootNav = lazy(() => rawFindByFunctionProps("getRootNavigationRef")); -const getRouting = lazy(() => rawFindByFunctionProps("transitionToGuild")); -const getCreateJoinGuildMod = lazy(() => rawFind((m: any) => typeof m?.handleCreateJoinGuildPress === "function")); -const getCirclePlusIcon = lazy(() => rawFind((m: any) => m?.CirclePlusIcon)?.CirclePlusIcon); +const Flux = findByProps("useStateFromStores"); +const SortedGuildStore = findByStoreName("SortedGuildStore"); +const RootNav = findByProps("getRootNavigationRef"); +const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes"); +const Routing = findByProps("transitionToGuild"); -const getExternalContext = lazy(() => rawFind((m: any) => m?.QuestDockExternalCoordinationContext)?.QuestDockExternalCoordinationContext); -const getQuestDockMode = lazy(() => rawFind((m: any) => m?.QuestDockMode?.COLLAPSED != null)?.QuestDockMode); -const getSortedGuildStore = lazy(() => rawFind((m: any) => typeof m?.getName === "function" && m.getName.length === 0 && m.getName() === "SortedGuildStore")); +const CreateJoinGuildMod = find((m: any) => typeof m?.handleCreateJoinGuildPress === "function"); +const CirclePlusIcon = find((m: any) => m?.CirclePlusIcon)?.CirclePlusIcon; +const rawColors = findByProps("colors", "unsafe_rawColors")?.unsafe_rawColors; + +const createJoinBg = rawColors?.GREEN_360; +const createJoinIconColor = rawColors?.WHITE; + +const ExternalCoordinationMod = find((m: any) => m?.QuestDockExternalCoordinationContext); +const ExternalContext = ExternalCoordinationMod?.QuestDockExternalCoordinationContext; +const QuestDockMode = find((m: any) => m?.QuestDockMode?.COLLAPSED != null)?.QuestDockMode; const ICON = 48; const GAP = 6; @@ -35,21 +39,17 @@ function CreateJoinButton() { }, [scale]); const onPress = React.useCallback(() => { - const haptic = getHaptic(); - haptic?.triggerHapticFeedback?.(haptic.HapticFeedbackTypes.SOFT); - getCreateJoinGuildMod()?.handleCreateJoinGuildPress?.(); + Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.SOFT); + CreateJoinGuildMod?.handleCreateJoinGuildPress?.(); }, []); - const CirclePlusIcon = getCirclePlusIcon(); - const rawColors = getColorModule()?.unsafe_rawColors; - return ( - + {CirclePlusIcon ? ( - + ) : ( - {"+"} + {"+"} )} @@ -60,20 +60,14 @@ export default function ServerDrawerSheet({ gestureContext }: { gestureContext: useProxy(storage); const pick = React.useCallback((id: string) => { - const haptic = getHaptic(); - haptic?.triggerHapticFeedback?.(haptic.HapticFeedbackTypes.SOFT); - - const routing = getRouting(); - if (routing?.transitionToGuild) { - routing.transitionToGuild(id); + Haptic?.triggerHapticFeedback(Haptic.HapticFeedbackTypes.SOFT); + if (Routing?.transitionToGuild) { + Routing.transitionToGuild(id, null); } else { - getRootNav()?.getRootNavigationRef?.()?.navigate("guilds", { guildId: id }); + RootNav?.getRootNavigationRef()?.navigate("guilds", { guildId: id }); } }, []); - const Flux = getFlux(); - const SortedGuildStore = getSortedGuildStore(); - const nodes: GuildNode[] = Flux?.useStateFromStores?.( [SortedGuildStore], () => { @@ -91,14 +85,12 @@ export default function ServerDrawerSheet({ gestureContext }: { gestureContext: if (minH.get() !== h) minH.set(h); }, [minH]); - const ExternalContext = getExternalContext(); const extCtx = ExternalContext ? React.useContext(ExternalContext) as any : null; const setMode = extCtx?.setRestingQuestDockMode; const specs = ctx?.questDockWrapperSpecs; React.useEffect(() => { - const QuestDockMode = getQuestDockMode(); if (!setMode || !QuestDockMode || !specs) return; const sub = BackHandler.addEventListener("hardwareBackPress", () => { const h = specs.get()?.height ?? 56; diff --git a/plugins/server-drawer/src/index.ts b/plugins/server-drawer/src/index.ts index f1014c6..6b04ba5 100644 --- a/plugins/server-drawer/src/index.ts +++ b/plugins/server-drawer/src/index.ts @@ -1,4 +1,3 @@ -import { logger } from "@vendetta"; import { storage } from "@vendetta/plugin"; import { patchFakeQuestDock } from "./patches/fakeQuestDock"; import { patchQuestDockSlot, patchEmpty } from "./patches/contentPatch"; @@ -7,85 +6,31 @@ import { patchCreateElement } from "./lib/createElementIntercept"; import { patchAutoCollapseFolders } from "./patches/autoCollapseFolders"; import Settings from "./ui/Settings"; +const TAG = "[ServerDrawer]"; let cleanups: (() => void)[] = []; -let retryHandle: ReturnType | undefined; -// Each of these functions already checks its own lookup and returns false instead of patching -// when it comes up empty - this wrapper adds one more layer so a lookup that throws outright -// (e.g. calling a property off an undefined module) can't take the rest of onLoad's patches -// down with it, same as every other plugin in this repo. -function tryPatch(name: string, fn: () => boolean): boolean { - try { - return fn(); - } catch (e) { - logger.error(`[ServerDrawer] "${name}" patch threw, skipping: ${e}`); - return false; - } -} - -// The Quest Dock (and the internals it depends on) isn't guaranteed to be registered in Metro yet -// at the exact moment onLoad runs - this is exactly why the original plugin needed a server -// tapped before the drawer would show: every one of its lookups was a one-shot find() with no -// retry, so a module that hadn't registered yet at that instant stayed unresolved for the rest of -// the session. Retrying whatever hasn't landed yet on a fast interval until everything's applied -// (or it gives up after ~10s) means the drawer shows up on its own instead of needing a tap to -// nudge Discord into registering the modules this plugin is waiting on. function applyAll() { - const patchers: Record boolean> = { - ...(storage.fakeQuestDock ? { fakeQuestDock: () => patchFakeQuestDock(cleanups) } : {}), - expanded: () => patchQuestDockSlot("QuestDockContentExpanded", cleanups), - collapsed: () => patchQuestDockSlot("QuestDockContentCollapsed", cleanups), - enrolledHeader: () => patchEmpty("QuestDockEnrolledHeader", cleanups), - unenrolledHeader: () => patchEmpty("QuestDockUnenrolledHeader", cleanups), - enrolledBody: () => patchEmpty("QuestDockEnrolledBody", cleanups), - unenrolledBody: () => patchEmpty("QuestDockUnenrolledBody", cleanups), - autoCollapseFolders: () => patchAutoCollapseFolders(cleanups), - ...(storage.hideGuildsBar ? { hideGuildsBar: () => patchHideGuildsBar(cleanups) } : {}), - }; + console.log(TAG, "onLoad"); + + let patched = 0; patchCreateElement(cleanups); + patched++; - const pending = new Set(Object.keys(patchers)); - const applied = new Set(); + if (storage.fakeQuestDock !== false && patchFakeQuestDock(cleanups)) patched++; + if (patchQuestDockSlot("QuestDockContentExpanded", cleanups)) patched++; + if (patchQuestDockSlot("QuestDockContentCollapsed", cleanups)) patched++; + if (patchEmpty("QuestDockEnrolledHeader", cleanups)) patched++; + if (patchEmpty("QuestDockUnenrolledHeader", cleanups)) patched++; + if (patchEmpty("QuestDockEnrolledBody", cleanups)) patched++; + if (patchEmpty("QuestDockUnenrolledBody", cleanups)) patched++; + if (patchAutoCollapseFolders(cleanups)) patched++; + if (storage.hideGuildsBar !== false && patchHideGuildsBar(cleanups)) patched++; - const attempt = () => { - for (const name of pending) { - if (tryPatch(name, patchers[name])) { - applied.add(name); - pending.delete(name); - } - } - - if (pending.size === 0 && retryHandle) { - clearInterval(retryHandle); - retryHandle = undefined; - } - }; - - attempt(); - - if (pending.size > 0) { - let ticks = 0; - retryHandle = setInterval(() => { - attempt(); - if (++ticks >= 50 && retryHandle) { // ~10s at 200ms - clearInterval(retryHandle); - retryHandle = undefined; - if (pending.size > 0) { - logger.error(`[ServerDrawer] Gave up waiting on: ${[...pending].join(", ")}`); - } - } - }, 200); - } - - logger.log(`[ServerDrawer] onLoad: applied immediately - ${[...applied].join(", ") || "none"}${pending.size ? `, still waiting on ${[...pending].join(", ")}` : ""}`); + console.log(TAG, `onLoad done - ${patched} patches applied, ${cleanups.length} cleanups`); } function unpatchAll() { - if (retryHandle) { - clearInterval(retryHandle); - retryHandle = undefined; - } for (const fn of cleanups) { try { fn(); diff --git a/plugins/server-drawer/src/lib/createElementIntercept.ts b/plugins/server-drawer/src/lib/createElementIntercept.ts index c17b749..1be1ed4 100644 --- a/plugins/server-drawer/src/lib/createElementIntercept.ts +++ b/plugins/server-drawer/src/lib/createElementIntercept.ts @@ -1,355 +1,54 @@ import { React } from "@vendetta/metro/common"; -import { after } from "@vendetta/patcher"; interface Intercept { replacement: React.ComponentType; extraProps?: Record; - /** - * How many ancestor levels above this element are allowed to be collapsed to zero size along - * with it (0 = none). See `COLLAPSE_STYLE` below for why replacing a component with one that - * renders nothing is, on its own, not enough to reclaim its space. - */ - collapseAncestors?: number; -} - -interface PropsIntercept { - predicate: (props: any) => boolean; - replacement: React.ComponentType | null; -} - -interface PropsTransform { - predicate: (props: any) => boolean; - transform: (props: any) => any; -} - -interface TypeDetector { - key: string; - predicate: (type: any) => boolean; - onDetected: (type: any) => void; - persistent: boolean; } const intercepts = new Map, Intercept>(); -const propsIntercepts: PropsIntercept[] = []; -const propsTransforms: PropsTransform[] = []; -let typeDetectors: TypeDetector[] = []; -const detectorKeys = new Set(); +let origCreateElement: typeof React.createElement | null = null; let isPatched = false; -/** - * A React element created for something we're hiding, mapped to how many more ancestor levels may - * still be collapsed with it. Elements are plain objects and are created child-first (a parent's - * jsx() call can only run once its children exist), so a mark placed on a child is always already - * present by the time the parent's own call is intercepted - which is what makes walking the - * collapse *upward* possible at element-creation time at all. - */ -const collapseMarks = new WeakMap(); - -/** - * Zeroes an element out in every direction a parent could have sized it. `display: "none"` alone - * is enough on Yoga, but the explicit width/flex zeroes also cover a container that sets its - * child's size through a style array further up, and `overflow: hidden` stops anything absolutely - * positioned inside from still painting. - */ -const COLLAPSE_STYLE = { - display: "none" as const, - width: 0, - minWidth: 0, - maxWidth: 0, - flexGrow: 0, - flexShrink: 0, - flexBasis: 0, - margin: 0, - padding: 0, - borderWidth: 0, - overflow: "hidden" as const, -}; - export function registerIntercept( original: React.ComponentType, replacement: React.ComponentType, extraProps?: Record, - options?: { collapseAncestors?: number }, ) { - intercepts.set(original, { replacement, extraProps, collapseAncestors: options?.collapseAncestors }); -} - -// Same idea as registerIntercept, but matches on a predicate over the element's props instead of -// an exact type reference - useful when the component has no reliable name to find it by, but -// always passes some distinguishing prop. Pass replacement: null to render nothing instead. -export function registerPropsIntercept(predicate: (props: any) => boolean, replacement: React.ComponentType | null) { - propsIntercepts.push({ predicate, replacement }); -} - -// Rewrites an element's props in place, keeping the same type - unlike registerPropsIntercept, -// this doesn't swap in a different component, it just lets the real one render with different -// input. Exists because Quest Dock's hooks are called through a same-chunk closure reference, not -// the exported property - patching the export does nothing, but every element creation goes -// through this one shared path regardless of chunk, so there's no equivalent staleness here. -export function registerPropsTransform(predicate: (props: any) => boolean, transform: (props: any) => any) { - propsTransforms.push({ predicate, transform }); -} - -// Purely observational: the first time an element is created whose type matches predicate, calls -// onDetected(type) with the live reference. Doesn't touch rendering - pair it with -// registerIntercept/registerPropsIntercept in the callback for that. Exists for components that -// aren't findable by searching Metro's registry after the fact (races against first mount). -// -// key dedupes registrations - index.ts retries every 200ms for ~10s, and duplicate detectors used -// to pile up, each re-tested against every element created anywhere in the app. -// -// persistent keeps the detector alive for every future match instead of firing once. GuildsBar's -// own type reference changes on some navigation paths (switching servers via this plugin's own -// drawer), so a one-shot detector would only ever catch the first, now-abandoned reference. -export function registerTypeDetector( - key: string, - predicate: (type: any) => boolean, - onDetected: (type: any) => void, - options?: { persistent?: boolean }, -) { - if (detectorKeys.has(key)) return; - detectorKeys.add(key); - typeDetectors.push({ key, predicate, onDetected, persistent: options?.persistent ?? false }); -} - -export function hasTypeDetector(key: string): boolean { - return detectorKeys.has(key); -} - -function runTypeDetectors(type: any) { - if (typeDetectors.length === 0) return; - - let consumed = false; - for (const detector of typeDetectors) { - let matched = false; - try { - matched = detector.predicate(type); - } catch { - // A bad predicate shouldn't block every other detector or every element from rendering. - } - if (!matched) continue; - - try { - detector.onDetected(type); - } catch { - // Same - a detector's own handler throwing shouldn't take anything else down. - } - if (!detector.persistent) { - detector.justFired = true; - consumed = true; - } - } - - // Only rebuild the list when a one-shot detector actually fired. The previous version rebuilt - // it on every single element creation app-wide, forever, because the GuildsBar detector is - // persistent and so the list is never empty - an allocation per element on the hottest path in - // the entire app. - if (consumed) { - typeDetectors = typeDetectors.filter((d) => d.persistent || !d.justFired); - } -} - -// Marking is done out-of-band so the filter above stays a pure predicate. -declare module "./createElementIntercept" {} -interface TypeDetector { justFired?: boolean } - -/** - * Returns the collapse depth a parent element should inherit from its children, or 0 for "leave - * this element alone." - * - * Returns the *maximum* collapse budget among real children. Previously this guarded against parents - * with multiple children, but the live GuildsBar rail has sibling wrappers (avatar/dm tile) inside - * the same outer container - the rail itself needs to be zeroed out too, and the replaced GuildsBar - * child is the only collapsed one. - */ -function inspectCollapseChild(child: any): number { - if (child == null || child === false || typeof child !== "object") return 0; - return collapseMarks.get(child) ?? 0; -} - -function inheritedCollapseDepth(props: any, rest: any[] = []): number { - let deepest = 0; - - const children = props?.children; - if (children != null) { - if (Array.isArray(children)) { - for (const child of children) deepest = Math.max(deepest, inspectCollapseChild(child)); - } else { - deepest = Math.max(deepest, inspectCollapseChild(children)); - } - } - - for (const child of rest) deepest = Math.max(deepest, inspectCollapseChild(child)); - return deepest; -} - -/** Returns a replacement type if this element should be intercepted, `null` to render nothing, or `undefined` to pass through unchanged. */ -function resolveReplacement(type: any, props: any, rest: any[]): { type: any; props: any; collapse?: number } | null | undefined { - runTypeDetectors(type); - - let effectiveProps = props; - if (effectiveProps) { - for (const { predicate, transform } of propsTransforms) { - try { - if (predicate(effectiveProps, type, rest)) { - effectiveProps = transform(effectiveProps); - } - } catch { - // A bad transform shouldn't block every other element from rendering. - } - } - } - - if (effectiveProps) { - for (const { predicate, replacement } of propsIntercepts) { - try { - if (predicate(effectiveProps, type, rest)) { - return replacement ? { type: replacement, props: effectiveProps } : null; - } - } catch { - // A bad predicate shouldn't block every other element from rendering. - } - } - } - - const entry = intercepts.get(type); - if (entry) { - return { - type: entry.replacement, - props: entry.extraProps ? { ...effectiveProps, ...entry.extraProps } : effectiveProps, - collapse: entry.collapseAncestors, - }; - } - - // Nothing to swap here, but if this element's only child is one we've already collapsed, this - // element is a wrapper that exists purely to hold it - zero it out too and pass the remaining - // budget further up. - const inherited = inheritedCollapseDepth(effectiveProps, rest); - if (inherited > 0) { - return { - type, - props: { ...effectiveProps, style: [effectiveProps?.style, COLLAPSE_STYLE] }, - collapse: inherited - 1, - }; - } - - if (effectiveProps !== props) { - return { type, props: effectiveProps }; - } - - return undefined; -} - -// Mutates the already-created element in place rather than substituting args before creation - -// after() only sees the return value, not the call itself. A raw property reassignment on the -// jsx-runtime object (what this used to do) doesn't reliably reach real render calls here; after() -// does. Confirmed live: GuildsBar creations were observed firing through this exact patch point, -// but res.type never actually changed - the intercept logic was correct, the patch mechanism wasn't. -function applyResolved(res: any, type: any, props: any, rest: any[]) { - if (!res || typeof res !== "object") return res; - const resolved = resolveReplacement(type, props, rest); - if (resolved === null) { - res.type = () => null; - return res; - } - if (resolved) { - res.type = resolved.type; - res.props = resolved.props ?? null; - if (resolved.collapse && resolved.collapse > 0) { - collapseMarks.set(res, resolved.collapse); - } - } - return res; + intercepts.set(original, { replacement, extraProps }); } export function patchCreateElement(cleanups: (() => void)[]) { if (isPatched) return; + + origCreateElement = React.createElement; + if (!origCreateElement) { + console.warn("[ServerDrawer] React.createElement is null, skipping patch"); + return; + } + + const patched = function (type: any, props: any, ...rest: any[]) { + if (!origCreateElement) { + return React.createElement(type, props, ...rest); + } + const entry = intercepts.get(type); + if (entry) { + const newProps = entry.extraProps + ? { ...props, ...entry.extraProps } + : props; + return origCreateElement.call(React, entry.replacement, newProps, ...rest); + } + return origCreateElement.call(React, type, props, ...rest); + }; + + Object.assign(patched, origCreateElement); + React.createElement = patched as typeof React.createElement; isPatched = true; - if (React.createElement) { - cleanups.push( - after("createElement", React, (args: any[], res: any) => applyResolved(res, args[0], args[1], args.slice(2))) - ); - } else { - console.warn("[ServerDrawer] React.createElement is null, skipping patch"); - } - - // Discord's bundle compiles JSX through the automatic runtime (jsx/jsxs/jsxDEV) instead of - // React.createElement, so patching createElement alone misses nearly everything. We scan for - // the runtime by shape rather than name since names get mangled, patch every copy we find (a - // bundle can have more than one), and keep scanning on an interval since Metro registers - // modules lazily - a copy that hasn't been required yet at boot just isn't there to find. - const patchedJsxRuntimes = new WeakSet(); - - function isJsxRuntime(m: any): boolean { - return typeof m?.jsx === "function" || typeof m?.jsxs === "function" || typeof m?.jsxDEV === "function"; - } - - function patchJsxObject(runtime: any) { - if (patchedJsxRuntimes.has(runtime)) return; - patchedJsxRuntimes.add(runtime); - for (const key of ["jsx", "jsxs", "jsxDEV"] as const) { - if (typeof runtime[key] !== "function") continue; - cleanups.push( - after(key, runtime, (args: any[], res: any) => applyResolved(res, args[0], args[1], args.slice(2))) - ); - } - } - - function patchJsxModule(def: any) { - if (!def?.publicModule?.exports) return; - const exports = def.publicModule.exports; - - try { - if (isJsxRuntime(exports)) patchJsxObject(exports); - - const dflt = exports.default; - if (dflt != null && isJsxRuntime(dflt)) patchJsxObject(dflt); - } catch { - // Ignore one module's bad shape and continue scanning. - } - } - - function scanAndPatchJsxRuntimes() { - const modules = (window as any)?.modules; - if (!modules) return; - for (const id in modules) { - const def = modules[id]; - if (!def?.isInitialized) continue; - patchJsxModule(def); - } - } - - // Patch any jsx runtimes already loaded before Discord's first render pass starts, synchronously - // - an async scan would complete too late for the very first render of an early-mounting target. - scanAndPatchJsxRuntimes(); - - // Fast while the app is still booting and chunks are being pulled in, then slow, then stop - - // a full window.modules walk is not free and there's nothing left to catch once the UI has - // settled. - let ticks = 0; - let timer: ReturnType | undefined = setInterval(() => { - scanAndPatchJsxRuntimes(); - if (++ticks === 50 && timer) { // ~5s at 100ms - clearInterval(timer); - timer = setInterval(() => { - scanAndPatchJsxRuntimes(); - if (++ticks >= 75 && timer) { // + ~25s at 1s - clearInterval(timer); - timer = undefined; - } - }, 1000); - } - }, 100); - cleanups.push(() => { - if (timer) clearInterval(timer); - timer = undefined; - isPatched = false; + if (isPatched && origCreateElement) { + React.createElement = origCreateElement; + isPatched = false; + } intercepts.clear(); - propsIntercepts.length = 0; - propsTransforms.length = 0; - typeDetectors = []; - detectorKeys.clear(); }); } diff --git a/plugins/server-drawer/src/patches/contentPatch.tsx b/plugins/server-drawer/src/patches/contentPatch.tsx index 1b32bfb..5aeec26 100644 --- a/plugins/server-drawer/src/patches/contentPatch.tsx +++ b/plugins/server-drawer/src/patches/contentPatch.tsx @@ -1,15 +1,10 @@ import { React } from "@vendetta/metro/common"; import { rawFind } from "../lib/rawFind"; import ServerDrawerSheet from "../components/ServerDrawerSheet"; -import { registerIntercept, registerTypeDetector } from "../lib/createElementIntercept"; +import { registerIntercept } from "../lib/createElementIntercept"; const TAG = "[ServerDrawer]"; -// rawFind, not @vendetta/metro's find - the original plugin's one-shot find() calls are exactly -// why it needed a server tapped before the drawer would show: if the target module hadn't -// registered in Metro yet at the exact moment onLoad ran, find()'s result (undefined) is cached -// forever and never rescanned, so index.tsx's retry loop calling this again was pointless. rawFind -// walks window.modules directly on every call instead, so retrying actually has a chance to work. let cachedGestureContext: any = null; function getGestureContext(): any { if (!cachedGestureContext) { @@ -23,66 +18,46 @@ function isNamed(name: string) { type?.type?.name === name || type?.type?.displayName === name; } -// Patches both QuestDockContentExpanded and QuestDockContentCollapsed to render the drawer, not -// just Expanded like the original. Confirmed live (Key Inspector) that restingQuestDockMode - the -// Reanimated value Discord uses to pick which of the two to render - starts as "collapsed" on a -// fresh load, not "expanded". Patching only Expanded left the drawer invisible behind whatever the -// collapsed variant showed (nothing, since that slot went through a separate null-patch) until -// something else happened to flip the mode. Swapping in the same drawer for both names sidesteps -// needing to fight that mode at all - whichever Discord picks, the drawer is what's there. export function patchQuestDockSlot(name: string, cleanups: (() => void)[]): boolean { - registerTypeDetector(`ServerDrawer.Slot.${name}`, isNamed(name), (real) => { - registerIntercept(real, ServerDrawerSheet, { gestureContext: getGestureContext() }); - console.log(TAG, `PATCH: ${name} replaced (type detector)`); - }, { persistent: true }); - const mod = rawFind((m) => isNamed(name)(m?.type)); if (!mod?.type) { - console.log(TAG, `WARN: ${name} not found yet (will retry)`); + console.log(TAG, `WARN: ${name} not found (will retry)`); return false; } - const orig = mod.type; + registerIntercept(orig, ServerDrawerSheet, { gestureContext: getGestureContext() }); + mod.type = function ServerDrawerPatch() { return ; }; cleanups.push(() => { mod.type = orig; }); - console.log(TAG, `PATCH: ${name} replaced (module mutation)`); + console.log(TAG, `PATCH: ${name} replaced`); return true; } -// Sub-sections inside Discord's own dock content (header/body variants) - kept as null-patches -// like the original. Once patchQuestDockSlot above wins, Discord's real header/body components -// never get created in the first place, so this is mostly moot, but harmless to keep as a backup -// for whichever of the two patches loses its own boot race. export function patchEmpty(name: string, cleanups: (() => void)[]): boolean { - registerTypeDetector(`ServerDrawer.Empty.${name}`, isNamed(name), (real) => { - registerIntercept(real, function EmptyPatch() { return null; }); - console.log(TAG, `PATCH: ${name} replaced (type detector)`); - }, { persistent: true }); - const mod = rawFind((m) => isNamed(name)(m?.type)); if (!mod?.type) { - console.log(TAG, `WARN: ${name} not found yet (will retry)`); + console.log(TAG, `WARN: ${name} not found`); return false; } - const orig = mod.type; - function EmptyPatch() { - return null; - } - mod.type = EmptyPatch; + const originalComponent = orig; - try { - Object.defineProperties(mod.type, Object.getOwnPropertyDescriptors(orig)); - } catch { - // Some property descriptors can't be copied onto another function - fine, this is a - // best-effort compatibility shim, not something correctness depends on. + mod.type = function EmptyPatch() { + return null; + }; + + if (originalComponent) { + try { + Object.defineProperties(mod.type, Object.getOwnPropertyDescriptors(originalComponent)); + } catch { + // Best-effort - not every descriptor can be copied onto another function. + } + mod.type.displayName = name; } - mod.type.displayName = name; cleanups.push(() => { mod.type = orig; }); - console.log(TAG, `PATCH: ${name} replaced (module mutation)`); return true; } diff --git a/plugins/server-drawer/src/patches/hideGuildsBar.tsx b/plugins/server-drawer/src/patches/hideGuildsBar.tsx index d5f984f..9de508f 100644 --- a/plugins/server-drawer/src/patches/hideGuildsBar.tsx +++ b/plugins/server-drawer/src/patches/hideGuildsBar.tsx @@ -1,122 +1,43 @@ -import React from "react"; -import { View } from "react-native"; -import { registerPropsTransform, registerTypeDetector, registerIntercept } from "../lib/createElementIntercept"; -import { captureFiberRef } from "../lib/fiberCapture"; +import { find, findByName } from "@vendetta/metro"; +import { registerIntercept } from "../lib/createElementIntercept"; const TAG = "[ServerDrawer]"; -const SD_NOTHING_TEST_ID = "ServerDrawerNothing"; -// Whether this capture point actually mounts depends entirely on whether the intercept below -// fires (unverified independently of it - see lib/fiberCapture.ts for the primary, confirmed -// capture point inside ServerDrawerSheet.tsx itself, which is guaranteed to mount whenever the -// drawer is visibly showing). Kept here too as a second chance, harmless either way. function Nothing() { - // Use the same aggressive collapse style as createElementIntercept's COLLAPSE_STYLE so that - // the replaced element itself takes up zero space while still mounting a ref for fiber capture. - // Keeping a mounted element with zero size lets us capture the fiber without leaving a visible - // 1px gap on the left rail. - return React.createElement(View, { - ref: captureFiberRef, - style: { - display: "none", - width: 0, - minWidth: 0, - maxWidth: 0, - flexGrow: 0, - flexShrink: 0, - flexBasis: 0, - margin: 0, - padding: 0, - borderWidth: 0, - overflow: "hidden", - }, + return null; +} + +function findGuildsBar(): any { + const byName = findByName("GuildsBar"); + if (byName) return { default: byName }; + + let mod = find((m) => { + try { return m?.default?.type?.name === "GuildsBar"; } catch { return false; } }); -} + if (mod?.default) return mod; -// Confirmed live (Key Inspector's fiber capture): the parent creates GuildsBar via its OUTER -// React.memo wrapper object ($$typeof: Symbol(react.memo)), which has no own .name/.displayName at -// all - those only exist on the memo's inner function, at type.type.name/type.type.displayName. -// Checking only the outer two (as the first version of this file did) never matches anything, -// which is why the intercept never fired despite the underlying mechanism being sound. -function isGuildsBar(type: any): boolean { - return type?.name === "GuildsBar" || type?.displayName === "GuildsBar" || - type?.type?.name === "GuildsBar" || type?.type?.displayName === "GuildsBar"; -} + mod = find((m) => { + try { return m?.default?.displayName === "GuildsBar"; } catch { return false; } + }); + if (mod?.default) return mod; -/** - * Reset after live, on-device verification (Key Inspector's fiber capture + Eval) disproved the - * previous approach's own premise. That version assumed GuildsBar had no discoverable runtime name - * and hid it via a literal nativeID prop instead - but a direct fiber-tree walk found the live, - * currently-mounted component's name is genuinely "GuildsBar" (tag 15, SimpleMemoComponent, single - * `enableHome` prop). The real bug: Metro-search-based lookups (rawFindByTypeName("GuildsBar")) DO - * find *something* by that name, but it's a different, unrelated module - confirmed by comparing - * the metro-search result against the live fiber's own `.type` by reference, which do not match. - * That's exactly the kind of silent wrong-match this whole repo has been burned by before. - * - * Also confirmed live: GuildsBar genuinely re-renders on normal usage (switching servers - 2 calls - * counted over a 15s window of real navigation), unlike YouBar's frozen-after-mount leaf. So unlike - * that investigation, there's no structural reason a straightforward patch can't work here - the - * only problem was ever finding the *correct* reference. - * - * registerTypeDetector sidesteps the module-registry name collision entirely: it inspects the - * `type` argument passed directly to createElement/jsx at the moment something actually creates an - * element with it, which is necessarily the real, live reference - there's nothing else it could be. - * Once caught, registerIntercept registers that *exact* reference (matched by identity, not name) - * so every future creation of it - which happens on every re-render of GuildsBar's parent, i.e. - * whenever the rail would normally repaint - renders nothing instead. - */ -function hasChildWithTestID(children: any, rest: any[], testID: string): boolean { - const inspect = (child: any) => child != null && typeof child === "object" && child.props?.testID === testID; - if (children != null) { - if (Array.isArray(children)) { if (children.some(inspect)) return true; } - else if (inspect(children)) return true; - } - for (const child of rest) if (inspect(child)) return true; - return false; + return null; } export function patchHideGuildsBar(cleanups: (() => void)[]): boolean { - // Hard-enforce the GuildsBar parent's visibility by detecting the hidden Nothing child and - // zeroing the rail wrapper itself. Confirmed live that the immediate parent carries an - // explicit width: 72 in its own style, so display: none alone left the space reserved - - // zeroing width/flex directly, the same way collapseAncestors does further up, actually - // reclaims it. - registerPropsTransform( - (props: any, _type: any, rest: any[]) => - hasChildWithTestID(props?.children, rest, SD_NOTHING_TEST_ID), - (props: any) => ({ - ...props, - style: [ - props?.style, - { - display: "none", - width: 0, - minWidth: 0, - maxWidth: 0, - flexGrow: 0, - flexShrink: 0, - flexBasis: 0, - margin: 0, - padding: 0, - borderWidth: 0, - overflow: "hidden", - }, - ], - }), - ); + const mod = findGuildsBar(); + if (!mod?.default) { + console.log(TAG, "WARN: GuildsBar not found"); + return false; + } + const orig = mod.default; - // Use a stable string key so duplicate registrations from retry loops are deduped correctly. - registerTypeDetector("ServerDrawer.HideGuildsBar", isGuildsBar, (realGuildsBar) => { - // Ask createElementIntercept to collapse a few ancestor levels as well so wrapper - // containers around the rail don't leave a leftover gap. - registerIntercept(realGuildsBar, Nothing, { testID: SD_NOTHING_TEST_ID }, { collapseAncestors: 6 }); - console.log(TAG, "PATCH: found a real GuildsBar reference, now rendering nothing"); - }, { persistent: true }); - cleanups.push(() => { - // No per-call unregister needed - the whole detector/intercept state gets cleared when - // createElementIntercept's own patch unwinds, part of the same cleanup pass this plugin - // already runs on unload/restart. - }); - console.log(TAG, "PATCH: watching for the real GuildsBar to appear"); + registerIntercept(orig, Nothing); + + mod.default = function HiddenGuildsBar() { + return null; + }; + mod.default.displayName = "GuildsBar"; + cleanups.push(() => { mod.default = orig; }); return true; } diff --git a/plugins/server-drawer/src/utils/theme.ts b/plugins/server-drawer/src/utils/theme.ts index 6451e67..049dfb4 100644 --- a/plugins/server-drawer/src/utils/theme.ts +++ b/plugins/server-drawer/src/utils/theme.ts @@ -1,28 +1,18 @@ -import { lazy } from "../lib/lazy"; -import { rawFindByStoreName } from "../lib/rawFind"; -import { getFlux, getColorModule } from "../lib/commonModules"; +import { findByProps, findByStoreName } from "@vendetta/metro"; -// getFlux/getColorModule come from lib/commonModules.ts - see that file for the decoy-module -// writeup. ExpandedGuildFolderStore switches from an eager, module-load-time findByStoreName (the -// caching finder, permanently wrong if it runs before the store module has registered) to lazy + -// rawFindByStoreName for the same timing reason established elsewhere in this plugin. -const getExpandedGuildFolderStore = lazy(() => rawFindByStoreName("ExpandedGuildFolderStore")); +const Flux = findByProps("useStateFromStores"); +const colors = findByProps("colors", "unsafe_rawColors")?.colors; +const ExpandedGuildFolderStore = findByStoreName("ExpandedGuildFolderStore"); export function useTheme() { - const colors = getColorModule()?.colors; return { - // TEXT_NORMAL is confirmed dead via Key Inspector (Discord renamed its whole semanticColors - // scheme) - TEXT_DEFAULT is its current replacement, kept alongside the old name in case an - // older Discord build is still in use. - text: colors?.TEXT_NORMAL ?? colors?.TEXT_DEFAULT ?? "#dbdee1", + text: colors?.TEXT_NORMAL ?? "#dbdee1", folder: "#5865f2", hover: colors?.STATE_LAYER_PRESS ?? "rgba(255,255,255,0.06)", }; } export function useFolderExpanded(folderId: string | number): boolean { - const Flux = getFlux(); - const ExpandedGuildFolderStore = getExpandedGuildFolderStore(); return Flux?.useStateFromStores?.( [ExpandedGuildFolderStore], () => {