From 58ddfc1f2d6647b166787b542fb936404729080d Mon Sep 17 00:00:00 2001 From: everestmcarthur <280397636+everestmcarthur@users.noreply.github.com> Date: Sun, 2 Aug 2026 09:30:25 -0400 Subject: [PATCH] ServerDrawer: fix memo-wrapper name detection and decoy-module lookups --- .../src/components/ContextMenuModal.tsx | 15 +++- .../server-drawer/src/components/DmTile.tsx | 34 +++++--- .../src/components/FolderItem.tsx | 18 +++-- .../src/components/GuildIcon.tsx | 15 +++- .../src/components/GuildItem.tsx | 34 +++++--- .../src/components/MarkAllReadButton.tsx | 29 ++++--- .../src/components/ServerDrawerSheet.tsx | 36 ++++----- plugins/server-drawer/src/lib/color.ts | 21 ++--- .../server-drawer/src/lib/commonModules.ts | 40 +++++++++ plugins/server-drawer/src/lib/rawFind.ts | 21 +++++ .../src/patches/contentPatch.tsx | 81 +++++++++---------- .../src/patches/hideGuildsBar.tsx | 8 +- plugins/server-drawer/src/utils/theme.ts | 15 +++- 13 files changed, 243 insertions(+), 124 deletions(-) create mode 100644 plugins/server-drawer/src/lib/commonModules.ts diff --git a/plugins/server-drawer/src/components/ContextMenuModal.tsx b/plugins/server-drawer/src/components/ContextMenuModal.tsx index 7ba1a10..891e590 100644 --- a/plugins/server-drawer/src/components/ContextMenuModal.tsx +++ b/plugins/server-drawer/src/components/ContextMenuModal.tsx @@ -1,11 +1,17 @@ import React from "react"; import { View, Text, Pressable, Image, Modal, StyleSheet, Dimensions, Animated } from "react-native"; import { semanticColors } from "@vendetta/ui"; -import { findByProps } from "@vendetta/metro"; import { resolveSemanticColor } from "../lib/color"; +import { lazy } from "../lib/lazy"; +import { rawFindByValidProps } from "../lib/rawFind"; +import { getHaptic } from "../lib/commonModules"; -const TextStyleSheet = findByProps("TextStyleSheet")?.TextStyleSheet; -const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes"); +// 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); function resolve(token: any, fallback: string): string { return resolveSemanticColor(token) ?? fallback; @@ -44,6 +50,9 @@ 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; diff --git a/plugins/server-drawer/src/components/DmTile.tsx b/plugins/server-drawer/src/components/DmTile.tsx index e460639..7391599 100644 --- a/plugins/server-drawer/src/components/DmTile.tsx +++ b/plugins/server-drawer/src/components/DmTile.tsx @@ -1,22 +1,30 @@ import React from "react"; import { View, Pressable, Image, StyleSheet } from "react-native"; -import { findByProps, findByName } from "@vendetta/metro"; import { getAssetIDByName } from "@vendetta/ui/assets"; +import { lazy } from "../lib/lazy"; +import { rawFindByFunctionProps, rawFindByName } from "../lib/rawFind"; +import { getFlux, getHaptic, getColorModule } from "../lib/commonModules"; const ICON = 48; const ChatIcon = getAssetIDByName("ChatIcon"); -const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes"); -const ChannelActions = findByProps("selectPrivateChannel"); -const SelectedChannelStore = findByName("SelectedChannelStore"); -const Flux = findByProps("useStateFromStores"); -const GuildStore = findByProps("getGuildId"); -const colors = findByProps("colors", "unsafe_rawColors")?.colors; + +// 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")); function openDms() { - Haptic?.triggerHapticFeedback(Haptic.HapticFeedbackTypes.SOFT); + const Haptic = getHaptic(); + Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.SOFT); + const ChannelActions = getChannelActions(); if (ChannelActions?.selectPrivateChannel) { - const lastChannelId = SelectedChannelStore?.getLastSelectedChannelId?.(); + const lastChannelId = getSelectedChannelStore()?.getLastSelectedChannelId?.(); ChannelActions.selectPrivateChannel(lastChannelId); } } @@ -25,9 +33,13 @@ function openDms() { // 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?.( - [GuildStore], - () => GuildStore?.getGuildId?.() == null, + [NavContext], + () => NavContext?.getGuildId?.() == null, ) ?? false; return ( diff --git a/plugins/server-drawer/src/components/FolderItem.tsx b/plugins/server-drawer/src/components/FolderItem.tsx index 5d3eb74..578949f 100644 --- a/plugins/server-drawer/src/components/FolderItem.tsx +++ b/plugins/server-drawer/src/components/FolderItem.tsx @@ -1,16 +1,21 @@ 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"; -const GuildActions = findByProps("toggleGuildFolderExpand"); -const Flux = findByProps("useStateFromStores"); -const GuildReadStateStore = findByStoreName("GuildReadStateStore"); +// 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 ICON = 48; const MINI = 16; @@ -30,6 +35,9 @@ function folderColor(color?: number | null): string { } function FolderBadge({ node }: { node: GuildNode }) { + const Flux = getFlux(); + const GuildReadStateStore = getGuildReadStateStore(); + const total = Flux?.useStateFromStores?.( [GuildReadStateStore], () => { @@ -97,7 +105,7 @@ export default function FolderItem({ node, onPick }: { node: GuildNode; onPick: const open = useFolderExpanded(node.id); const toggle = () => { - GuildActions?.toggleGuildFolderExpand?.(node.id); + getGuildActions()?.toggleGuildFolderExpand?.(node.id); }; if (open) { diff --git a/plugins/server-drawer/src/components/GuildIcon.tsx b/plugins/server-drawer/src/components/GuildIcon.tsx index d6d237e..2d28901 100644 --- a/plugins/server-drawer/src/components/GuildIcon.tsx +++ b/plugins/server-drawer/src/components/GuildIcon.tsx @@ -1,12 +1,19 @@ import React from "react"; import { View, Text, Image, StyleSheet } from "react-native"; -import { findByStoreName, findByProps } from "@vendetta/metro"; +import { lazy } from "../lib/lazy"; +import { rawFindByStoreName } from "../lib/rawFind"; +import { getFlux, getColorModule } from "../lib/commonModules"; -const GuildStore = findByStoreName("GuildStore"); -const Flux = findByProps("useStateFromStores"); -const colors = findByProps("colors", "unsafe_rawColors")?.colors; +// 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")); 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. diff --git a/plugins/server-drawer/src/components/GuildItem.tsx b/plugins/server-drawer/src/components/GuildItem.tsx index 5a0e55f..b2b9980 100644 --- a/plugins/server-drawer/src/components/GuildItem.tsx +++ b/plugins/server-drawer/src/components/GuildItem.tsx @@ -1,29 +1,31 @@ import React from "react"; import { View, Text, Pressable, Animated, StyleSheet } from "react-native"; -import { findByProps, findByStoreName } from "@vendetta/metro"; import { clipboard } from "@vendetta/metro/common"; import { storage } from "@vendetta/plugin"; import { useProxy } from "@vendetta/storage"; import { showToast } from "@vendetta/ui/toasts"; import { lazy } from "../lib/lazy"; -import { rawFindByProps } from "../lib/rawFind"; +import { rawFindByStoreName } from "../lib/rawFind"; +import { getFlux, getHaptic, getBulkAckMod, getReadStateTypesMod } from "../lib/commonModules"; import { GuildNode } from "../utils/theme"; import GuildIcon from "./GuildIcon"; import { ContextMenuModal, ContextMenuItem } from "./ContextMenuModal"; const ICON = 48; -const Flux = findByProps("useStateFromStores"); -const GuildReadStateStore = findByStoreName("GuildReadStateStore"); -const GuildStore = findByStoreName("GuildStore"); -const GuildChannelStore = findByStoreName("GuildChannelStore"); -const ChannelStore = findByStoreName("ChannelStore"); -const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes"); +// getFlux/getHaptic/getBulkAckMod/getReadStateTypesMod come from lib/commonModules.ts - see that +// file for the decoy-module writeup. +const getBulkAck = getBulkAckMod; +const getReadStateTypes = lazy(() => getReadStateTypesMod()?.ReadStateTypes); -// rawFindByProps, not findByProps, because these are retried via lazy() - see rawFind.ts for why -// a retried cached findByProps call is a no-op after its first failure. -const getBulkAck = lazy(() => rawFindByProps("bulkAck", "ackChannel")); -const getReadStateTypes = lazy(() => rawFindByProps("ReadStateTypes", "UnreadSetting")?.ReadStateTypes); +// These four switch 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 +// reason established earlier this session, not the decoy-module issue above - unverified whether the +// decoy fakes a getName() zero-arg store-name match too, so not claiming that's the cause here. +const getGuildReadStateStore = lazy(() => rawFindByStoreName("GuildReadStateStore")); +const getGuildStore = lazy(() => rawFindByStoreName("GuildStore")); +const getGuildChannelStore = lazy(() => rawFindByStoreName("GuildChannelStore")); +const getChannelStore = lazy(() => rawFindByStoreName("ChannelStore")); // Discord's own per-guild context menu (getGuildsBarGuildMenuItems) is a bare default export with // no named export - findByName/`.default.name` heuristics for it are unreliable, since a plain @@ -35,6 +37,8 @@ const getReadStateTypes = lazy(() => rawFindByProps("ReadStateTypes", "UnreadSet function markGuildRead(guildId: string) { const bulkAck = getBulkAck(); const ReadStateTypes = getReadStateTypes(); + const GuildChannelStore = getGuildChannelStore(); + const ChannelStore = getChannelStore(); if (!bulkAck?.bulkAck || ReadStateTypes?.CHANNEL == null || !GuildChannelStore?.getSelectableChannelIds) { showToast("Couldn't find Discord's read-state action - this may be unavailable on your version.", undefined); return; @@ -62,6 +66,9 @@ function markGuildRead(guildId: string) { function Badge({ guildId }: { guildId: string }) { useProxy(storage); + const Flux = getFlux(); + const GuildReadStateStore = getGuildReadStateStore(); + const mentionCount = Flux?.useStateFromStores?.( [GuildReadStateStore], () => GuildReadStateStore?.getMentionCount?.(guildId) ?? 0, @@ -113,7 +120,7 @@ export default function GuildItem({ node, onPick }: { node: GuildNode; onPick: ( const showMenu = React.useCallback(() => { const guildId = node.id as string; - const guild = GuildStore?.getGuild?.(guildId); + const guild = getGuildStore()?.getGuild?.(guildId); if (!guild) return; const ref = viewRef.current as any; @@ -142,6 +149,7 @@ 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/MarkAllReadButton.tsx b/plugins/server-drawer/src/components/MarkAllReadButton.tsx index 6604729..44df37a 100644 --- a/plugins/server-drawer/src/components/MarkAllReadButton.tsx +++ b/plugins/server-drawer/src/components/MarkAllReadButton.tsx @@ -1,30 +1,33 @@ import React from "react"; import { Pressable, Image, StyleSheet } from "react-native"; -import { findByProps, findByStoreName } from "@vendetta/metro"; import { getAssetIDByName } from "@vendetta/ui/assets"; import { showToast } from "@vendetta/ui/toasts"; import { lazy } from "../lib/lazy"; -import { rawFindByProps } from "../lib/rawFind"; +import { rawFindByStoreName } from "../lib/rawFind"; +import { getHaptic, getBulkAckMod, getReadStateTypesMod } from "../lib/commonModules"; const ICON = 48; const CheckIcon = getAssetIDByName("CheckmarkLargeIcon"); -const SortedGuildStore = findByStoreName("SortedGuildStore"); -const GuildChannelStore = findByStoreName("GuildChannelStore"); -const ChannelStore = findByStoreName("ChannelStore"); -// Lazy + rawFindByProps (not findByProps) because bulkAck lives on an action-creator module that, -// like routing, may not be required by Discord's own code yet at the moment this plugin's bundle -// loads - and a retried findByProps call is a no-op after its first failure (see rawFind.ts). -const getHaptic = lazy(() => rawFindByProps("triggerHapticFeedback", "HapticFeedbackTypes")); -const getBulkAck = lazy(() => rawFindByProps("bulkAck", "ackChannel")); -const getReadStateTypes = lazy(() => rawFindByProps("ReadStateTypes", "UnreadSetting")?.ReadStateTypes); +// getHaptic/getBulkAckMod/getReadStateTypesMod come from lib/commonModules.ts - see that file for +// the decoy-module writeup. +const getBulkAck = getBulkAckMod; +const getReadStateTypes = lazy(() => getReadStateTypesMod()?.ReadStateTypes); + +// These three switch 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 reason established earlier this session, not the decoy-module issue above - +// unverified whether the decoy fakes a getName() zero-arg store-name match too. +const getSortedGuildStore = lazy(() => rawFindByStoreName("SortedGuildStore")); +const getGuildChannelStore = lazy(() => rawFindByStoreName("GuildChannelStore")); +const getChannelStore = lazy(() => rawFindByStoreName("ChannelStore")); function collectGuildIds(): string[] { // Same tree ServerDrawerSheet already renders from (SortedGuildStore.getGuildsTree()), so this // reaches every guild the drawer shows, folders included, using data already proven to resolve // correctly rather than a separately-shaped GuildStore method. - const tree = SortedGuildStore?.getGuildsTree?.(); + const tree = getSortedGuildStore()?.getGuildsTree?.(); const roots = (tree?.root?.children ?? []).filter((n: any) => n?.type !== "root"); const ids: string[] = []; @@ -46,6 +49,8 @@ function collectGuildIds(): string[] { function markAllServersRead() { const bulkAck = getBulkAck(); const ReadStateTypes = getReadStateTypes(); + const GuildChannelStore = getGuildChannelStore(); + const ChannelStore = getChannelStore(); if ( !bulkAck?.bulkAck || ReadStateTypes?.CHANNEL == null || !GuildChannelStore?.getSelectableChannelIds || !GuildChannelStore?.getVocalChannelIds diff --git a/plugins/server-drawer/src/components/ServerDrawerSheet.tsx b/plugins/server-drawer/src/components/ServerDrawerSheet.tsx index 37c7d04..9cad7b2 100644 --- a/plugins/server-drawer/src/components/ServerDrawerSheet.tsx +++ b/plugins/server-drawer/src/components/ServerDrawerSheet.tsx @@ -1,35 +1,24 @@ import React from "react"; import { View, Text, Pressable, Animated, Dimensions, StyleSheet, BackHandler } from "react-native"; -import { find, findByProps, findByStoreName } from "@vendetta/metro"; import { lazy } from "../lib/lazy"; -import { rawFind, rawFindByProps } from "../lib/rawFind"; +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"; import MarkAllReadButton from "./MarkAllReadButton"; -const Flux = findByProps("useStateFromStores"); -const SortedGuildStore = findByStoreName("SortedGuildStore"); - -// These are looked up lazily (retried on first real use, not cached the moment this file is -// required) because this whole module gets evaluated as soon as the plugin's bundle loads - which -// can be before Discord's own routing/navigation modules have been required yet. Crucially, they -// use rawFind/rawFindByProps (not @vendetta/metro's cached finders) - Revenge's own finders -// permanently cache a "not found" result and never rescan, so retrying a cached findByProps would -// just hit that poisoned cache every time instead of ever finding the real module once it -// registers (confirmed by reading Revenge's own metro finder source). This is what actually made -// tapping a server in the drawer silently do nothing even after adding retries. -const getRootNav = lazy(() => rawFindByProps("getRootNavigationRef")); -const getHaptic = lazy(() => rawFindByProps("triggerHapticFeedback", "HapticFeedbackTypes")); -const getRouting = lazy(() => rawFindByProps("transitionToGuild")); +// 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 getRawColors = lazy(() => rawFindByProps("colors", "unsafe_rawColors")?.unsafe_rawColors); -const ExternalCoordinationMod = find((m: any) => m?.QuestDockExternalCoordinationContext); -const ExternalContext = ExternalCoordinationMod?.QuestDockExternalCoordinationContext; -const QuestDockMode = find((m: any) => m?.QuestDockMode?.COLLAPSED != null)?.QuestDockMode; +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 ICON = 48; const GAP = 6; @@ -51,7 +40,7 @@ function CreateJoinButton() { }, []); const CirclePlusIcon = getCirclePlusIcon(); - const rawColors = getRawColors(); + const rawColors = getColorModule()?.unsafe_rawColors; return ( @@ -85,6 +74,9 @@ export default function ServerDrawerSheet({ gestureContext }: { gestureContext: } }, []); + const Flux = getFlux(); + const SortedGuildStore = getSortedGuildStore(); + const nodes: GuildNode[] = Flux?.useStateFromStores?.( [SortedGuildStore], () => { @@ -102,12 +94,14 @@ 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/lib/color.ts b/plugins/server-drawer/src/lib/color.ts index 9bfad9f..9f0aa73 100644 --- a/plugins/server-drawer/src/lib/color.ts +++ b/plugins/server-drawer/src/lib/color.ts @@ -1,21 +1,24 @@ -import { find, findByStoreName } from "@vendetta/metro"; import { semanticColors } from "@vendetta/ui"; +import { lazy } from "./lazy"; +import { rawFind, rawFindByStoreName } from "./rawFind"; export const HEX_REGEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; -const ThemeStore = findByStoreName("ThemeStore"); +// Switched from the caching findByStoreName/find (permanently wrong if either runs before its +// target module registers) to lazy + raw variants for the same timing reason established +// elsewhere in this plugin. +const getThemeStore = lazy(() => rawFindByStoreName("ThemeStore")); // @vendetta/ui exposes the semanticColors token map but not a resolver function for it. -const rawResolveSemanticColor: (theme: any, semanticColor: any) => string | undefined = - find((m: any) => m?.default?.internal?.resolveSemanticColor)?.default?.internal?.resolveSemanticColor ?? - find((m: any) => m?.meta?.resolveSemanticColor)?.meta?.resolveSemanticColor ?? - (() => undefined); +const getRawResolveSemanticColor = lazy((): ((theme: any, semanticColor: any) => string | undefined) | undefined => + rawFind((m: any) => m?.default?.internal?.resolveSemanticColor)?.default?.internal?.resolveSemanticColor ?? + rawFind((m: any) => m?.meta?.resolveSemanticColor)?.meta?.resolveSemanticColor); /** Resolves a semanticColors[TOKEN_NAME] descriptor to a real color for the current theme. */ -export function resolveSemanticColor(token: any, theme: any = ThemeStore?.theme): string | undefined { +export function resolveSemanticColor(token: any, theme: any = getThemeStore()?.theme): string | undefined { if (!token) return undefined; try { - return rawResolveSemanticColor(theme, token); + return getRawResolveSemanticColor()?.(theme, token); } catch { return undefined; } @@ -26,7 +29,7 @@ export function resolveSemanticColor(token: any, theme: any = ThemeStore?.theme) * hardcoded hex color if every one of them is missing or fails to resolve - Discord renames these * tokens across versions often enough that a single hardcoded name isn't reliable on its own. */ -export function resolveSemanticColorSafe(tokenNames: string[], fallbackHex: string, theme: any = ThemeStore?.theme): string { +export function resolveSemanticColorSafe(tokenNames: string[], fallbackHex: string, theme: any = getThemeStore()?.theme): string { for (const name of tokenNames) { const token = (semanticColors as Record | undefined)?.[name]; if (!token) continue; diff --git a/plugins/server-drawer/src/lib/commonModules.ts b/plugins/server-drawer/src/lib/commonModules.ts new file mode 100644 index 0000000..9a67a9c --- /dev/null +++ b/plugins/server-drawer/src/lib/commonModules.ts @@ -0,0 +1,40 @@ +import { lazy } from "./lazy"; +import { rawFindByFunctionProps, rawFindByValidProps } from "./rawFind"; + +/** + * Confirmed live (Key Inspector's Eval console, full window.modules scan): a single decoy module + * matches dozens of common hook/utility property names at once - useStateFromStores, + * triggerHapticFeedback, colors, transitionToGuild, bulkAck, and more - all with non-function/ + * placeholder values (a type-shape/declaration-style object, not real code). Since window.modules + * iterates in ascending numeric id order and this decoy has a lower id than every real + * implementation it collides with, a plain "does this property exist" check + * (findByProps/rawFindByProps) reliably matches the decoy instead of the real thing - every single + * time, not occasionally. This is what made tapping a server, marking as read, and haptics all + * silently no-op even after this repo's own retry-loop fixes. + * + * rawFindByFunctionProps requires the matched property to actually be a function, which the decoy's + * isn't. Enum/object-shaped properties (which the decoy also fakes) need rawFindByValidProps + * instead, checking for a real value the decoy doesn't have. + * + * Centralized here because every component in this plugin independently needed the same handful of + * lookups - keeping one copy means the decoy-avoidance logic can't drift out of sync across files. + */ +export const getFlux = lazy(() => rawFindByFunctionProps("useStateFromStores")); + +export const getHaptic = lazy(() => rawFindByValidProps({ + triggerHapticFeedback: (v) => typeof v === "function", + HapticFeedbackTypes: (v) => v?.SOFT !== undefined, +})); + +/** Has both `.colors` (semantic tokens like TEXT_NORMAL/BG_ACCENT) and `.unsafe_rawColors` (raw palette like WHITE/GREEN_360) on the same module. */ +export const getColorModule = lazy(() => rawFindByValidProps({ + colors: (v) => v?.BG_ACCENT !== undefined, + unsafe_rawColors: (v) => v?.WHITE !== undefined, +})); + +export const getBulkAckMod = lazy(() => rawFindByFunctionProps("bulkAck", "ackChannel")); + +export const getReadStateTypesMod = lazy(() => rawFindByValidProps({ + ReadStateTypes: (v) => v?.CHANNEL !== undefined, + UnreadSetting: (v) => v?.ALL_MESSAGES !== undefined, +})); diff --git a/plugins/server-drawer/src/lib/rawFind.ts b/plugins/server-drawer/src/lib/rawFind.ts index 42c6378..178a371 100644 --- a/plugins/server-drawer/src/lib/rawFind.ts +++ b/plugins/server-drawer/src/lib/rawFind.ts @@ -68,6 +68,27 @@ export function rawFindByFunctionProps(...props: string[]): T | undefin return rawFind((m) => props.every((p) => typeof m?.[p] === "function")); } +/** + * Same idea as rawFindByFunctionProps, but for properties whose real value is an object/enum, not + * a function (e.g. ReadStateTypes, HapticFeedbackTypes) - "is it a function" can't discriminate + * those from the same decoy module rawFindByFunctionProps was built for, since the decoy's version + * is *also* an object. Confirmed live that the decoy's enum-shaped properties don't hold the real + * values (e.g. its ReadStateTypes/UnreadSetting come back null/empty on direct access, despite + * matching a plain "property exists" check) - so this instead requires each named property to pass + * its own validator against a value only the real implementation would actually have, e.g.: + * rawFindByValidProps({ ReadStateTypes: (v) => v?.CHANNEL !== undefined }). + */ +export function rawFindByValidProps(validators: Record boolean>): T | undefined { + const props = Object.keys(validators); + return rawFind((m) => props.every((p) => { + try { + return validators[p](m?.[p]); + } catch { + return false; + } + })); +} + // Matches Revenge's own byStoreName filter (metro/filters.ts): a zero-arg getName() returning // the store's name. export function rawFindByStoreName(name: string): T | undefined { diff --git a/plugins/server-drawer/src/patches/contentPatch.tsx b/plugins/server-drawer/src/patches/contentPatch.tsx index fbaec3d..227fd9f 100644 --- a/plugins/server-drawer/src/patches/contentPatch.tsx +++ b/plugins/server-drawer/src/patches/contentPatch.tsx @@ -1,14 +1,10 @@ import { React } from "@vendetta/metro/common"; import { rawFind } from "../lib/rawFind"; import ServerDrawerSheet from "../components/ServerDrawerSheet"; -import { registerIntercept } from "../lib/createElementIntercept"; +import { registerIntercept, registerTypeDetector } from "../lib/createElementIntercept"; const TAG = "[ServerDrawer]"; -// rawFind, not @vendetta/metro's find - both patchExpanded and patchEmpty are retried by index.ts's -// fast-retry loop, and Revenge's own find() permanently caches a "not found" result per call site -// and never rescans (see rawFind.ts), which would make the retry pointless after the first -// failed attempt. let cachedGestureContext: any = null; function getGestureContext(): any { if (!cachedGestureContext) { @@ -17,47 +13,50 @@ function getGestureContext(): any { return cachedGestureContext; } -export function patchExpanded( - cleanups: (() => void)[] -): boolean { - const mod = rawFind((m) => m?.type?.displayName === "QuestDockContentExpanded" || m?.type?.name === "QuestDockContentExpanded"); - if (!mod?.type) { - console.log(TAG, "WARN: QuestDockContentExpanded not found (will retry)"); - return false; - } - const orig = mod.type; +// Same fix as hideGuildsBar.tsx: memo-wrapped components are passed to createElement/jsx as the +// outer wrapper object, which has no own .name/.displayName - only its nested .type does. Missing +// that nested check meant this never matched anything, which is why Discord's real +// QuestDockEnrolledHeader (and friends) kept running unpatched against the fake quest data. +function isNamed(name: string) { + return (type: any) => type?.name === name || type?.displayName === name || + type?.type?.name === name || type?.type?.displayName === name; +} - registerIntercept(orig, ServerDrawerSheet, { gestureContext: getGestureContext() }); +function Nothing() { + return null; +} - mod.type = function ServerDrawerPatch() { - return ; - }; - cleanups.push(() => { mod.type = orig; }); - console.log(TAG, "PATCH: QuestDockContentExpanded replaced"); +/** + * Reset after live verification (Key Inspector's fiber capture + Eval, same methodology that found + * GuildsBar's bug) showed this file's original approach shares the exact same flaw: rawFind matching + * a component by `.type.displayName`/`.type.name` across window.modules can - and here, does - match + * a different, unrelated module than the one actually mounted. The visible symptom: Discord's real + * QuestDockEnrolledHeader (and friends) kept running unpatched, then crashed reading properties off + * the fake quest data useMobileQuestDock.ts hands out, because our replacement was silently mutating + * the wrong object the whole time. + * + * registerTypeDetector sidesteps this the same way it does for GuildsBar: 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 no module-registry ambiguity + * possible. registerIntercept then swaps that exact reference (matched by identity) for the + * replacement on every future creation. + */ +export function patchExpanded(cleanups: (() => void)[]): boolean { + registerTypeDetector(isNamed("QuestDockContentExpanded"), (real) => { + registerIntercept(real, ServerDrawerSheetWrapper); + console.log(TAG, "PATCH: found the real QuestDockContentExpanded, now rendering the drawer"); + }); return true; } -export function patchEmpty( - name: string, - cleanups: (() => void)[] -): boolean { - const mod = rawFind((m) => m?.type?.displayName === name || m?.type?.name === name); - if (!mod?.type) { - console.log(TAG, `WARN: ${name} not found`); - return false; - } - const orig = mod.type; - const originalComponent = orig; +function ServerDrawerSheetWrapper() { + return ; +} - mod.type = function EmptyPatch() { - return null; - }; - - if (originalComponent) { - Object.defineProperties(mod.type, Object.getOwnPropertyDescriptors(originalComponent)); - mod.type.displayName = name; - } - - cleanups.push(() => { mod.type = orig; }); +export function patchEmpty(name: string, cleanups: (() => void)[]): boolean { + registerTypeDetector(isNamed(name), (real) => { + registerIntercept(real, Nothing); + console.log(TAG, `PATCH: found the real ${name}, now rendering nothing`); + }); return true; } diff --git a/plugins/server-drawer/src/patches/hideGuildsBar.tsx b/plugins/server-drawer/src/patches/hideGuildsBar.tsx index 0346447..da3cdf2 100644 --- a/plugins/server-drawer/src/patches/hideGuildsBar.tsx +++ b/plugins/server-drawer/src/patches/hideGuildsBar.tsx @@ -6,8 +6,14 @@ function Nothing() { return null; } +// 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"; + return type?.name === "GuildsBar" || type?.displayName === "GuildsBar" || + type?.type?.name === "GuildsBar" || type?.type?.displayName === "GuildsBar"; } /** diff --git a/plugins/server-drawer/src/utils/theme.ts b/plugins/server-drawer/src/utils/theme.ts index 049dfb4..20b3f82 100644 --- a/plugins/server-drawer/src/utils/theme.ts +++ b/plugins/server-drawer/src/utils/theme.ts @@ -1,10 +1,15 @@ -import { findByProps, findByStoreName } from "@vendetta/metro"; +import { lazy } from "../lib/lazy"; +import { rawFindByStoreName } from "../lib/rawFind"; +import { getFlux, getColorModule } from "../lib/commonModules"; -const Flux = findByProps("useStateFromStores"); -const colors = findByProps("colors", "unsafe_rawColors")?.colors; -const ExpandedGuildFolderStore = findByStoreName("ExpandedGuildFolderStore"); +// 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")); export function useTheme() { + const colors = getColorModule()?.colors; return { text: colors?.TEXT_NORMAL ?? "#dbdee1", folder: "#5865f2", @@ -13,6 +18,8 @@ export function useTheme() { } export function useFolderExpanded(folderId: string | number): boolean { + const Flux = getFlux(); + const ExpandedGuildFolderStore = getExpandedGuildFolderStore(); return Flux?.useStateFromStores?.( [ExpandedGuildFolderStore], () => {