ServerDrawer: fix memo-wrapper name detection and decoy-module lookups

This commit is contained in:
everestmcarthur 2026-08-02 09:30:25 -04:00
parent 259b9b5b31
commit 58ddfc1f2d
13 changed files with 243 additions and 124 deletions

View file

@ -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<any>({
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;

View file

@ -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<any>("selectPrivateChannel"));
const getSelectedChannelStore = lazy(() => rawFindByName<any>("SelectedChannelStore"));
const getNavContext = lazy(() => rawFindByFunctionProps<any>("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 (

View file

@ -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<any>("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) {

View file

@ -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.

View file

@ -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]);

View file

@ -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

View file

@ -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 (
<Pressable onPress={onPress} onPressIn={scaleDown} onPressOut={scaleUp}>
@ -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;

View file

@ -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<any>("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<string, any> | undefined)?.[name];
if (!token) continue;

View file

@ -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<any>("useStateFromStores"));
export const getHaptic = lazy(() => rawFindByValidProps<any>({
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<any>({
colors: (v) => v?.BG_ACCENT !== undefined,
unsafe_rawColors: (v) => v?.WHITE !== undefined,
}));
export const getBulkAckMod = lazy(() => rawFindByFunctionProps<any>("bulkAck", "ackChannel"));
export const getReadStateTypesMod = lazy(() => rawFindByValidProps<any>({
ReadStateTypes: (v) => v?.CHANNEL !== undefined,
UnreadSetting: (v) => v?.ALL_MESSAGES !== undefined,
}));

View file

@ -68,6 +68,27 @@ export function rawFindByFunctionProps<T = any>(...props: string[]): T | undefin
return rawFind<T>((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<T = any>(validators: Record<string, (value: any) => boolean>): T | undefined {
const props = Object.keys(validators);
return rawFind<T>((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<T = any>(name: string): T | undefined {

View file

@ -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 <ServerDrawerSheet gestureContext={getGestureContext()} />;
};
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 <ServerDrawerSheet gestureContext={getGestureContext()} />;
}
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;
}

View file

@ -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";
}
/**

View file

@ -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],
() => {