ServerDrawer: add a fiber capture point guaranteed to actually mount

This commit is contained in:
everestmcarthur 2026-08-02 11:05:26 -04:00
parent 094f18d736
commit 5ad56932fc
3 changed files with 62 additions and 63 deletions

View file

@ -3,6 +3,7 @@ import { View, Text, Pressable, Animated, Dimensions, StyleSheet, BackHandler }
import { lazy } from "../lib/lazy"; import { lazy } from "../lib/lazy";
import { rawFind, rawFindByFunctionProps } from "../lib/rawFind"; import { rawFind, rawFindByFunctionProps } from "../lib/rawFind";
import { getFlux, getHaptic, getColorModule } from "../lib/commonModules"; import { getFlux, getHaptic, getColorModule } from "../lib/commonModules";
import { captureFiberRef } from "../lib/fiberCapture";
import { GuildNode } from "../utils/theme"; import { GuildNode } from "../utils/theme";
import GuildItem from "./GuildItem"; import GuildItem from "./GuildItem";
import FolderItem from "./FolderItem"; import FolderItem from "./FolderItem";
@ -122,6 +123,11 @@ export default function ServerDrawerSheet({ gestureContext }: { gestureContext:
return ( return (
<View style={st.alignTop}> <View style={st.alignTop}>
{/* Invisible - see lib/fiberCapture.ts. This is the reliable capture point: it's
guaranteed to mount whenever the drawer itself is visibly rendering, independent of
whether hideGuildsBar.tsx's own capture point (which depends on that separate
intercept actually firing) works or not. */}
<View ref={captureFiberRef} style={{ width: 1, height: 1 }} />
<View <View
style={[st.grid, { paddingHorizontal: padX, gap: GAP }]} style={[st.grid, { paddingHorizontal: padX, gap: GAP }]}
onLayout={onLayout} onLayout={onLayout}

View file

@ -0,0 +1,51 @@
/**
* Reads React's internal fiber pointer directly off a native view instance and walks up to the
* root, storing both under window.__serverDrawerFiberRoot/__serverDrawerFiberSelf for Eval scripts
* to search afterward. Same technique Key Inspector's own Settings-screen capture uses, but kept
* under separate globals - Key Inspector's capture only reaches whatever tree its own Settings
* screen renders in (very likely a separate native modal/root from MainTabs' guild tab content),
* so a capture point actually inside this plugin's own rendered output is what's needed to reach
* anything under the Guilds tab (confirmed live: a Settings-screen capture couldn't find
* QuestDockWithQuestContext at all, even navigating to the Guilds tab first).
*/
export function captureFiberRef(instance: any) {
if (!instance) return;
try {
let fiber: any = null;
if (instance._internalFiberInstanceHandleDEV) {
fiber = instance._internalFiberInstanceHandleDEV;
} else if (instance._internalInstanceHandle) {
fiber = instance._internalInstanceHandle;
} else if (instance.__internalInstanceHandle) {
fiber = instance.__internalInstanceHandle;
} else {
const fiberKey = Object.keys(instance).find(
(k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")
);
if (fiberKey) fiber = instance[fiberKey];
}
if (!fiber) return;
if (fiber.return === undefined && fiber.child === undefined) {
const nested = fiber.stateNode ?? fiber.fiber ?? fiber._debugOwner;
if (nested && (nested.return !== undefined || nested.child !== undefined)) {
fiber = nested;
} else {
return;
}
}
let root = fiber;
let guard = 0;
while (root.return && guard < 5000) {
root = root.return;
guard++;
}
(window as any).__serverDrawerFiberRoot = root;
(window as any).__serverDrawerFiberSelf = fiber;
} catch {
// Best-effort diagnostic capture - a failure here shouldn't affect the caller's own render.
}
}

View file

@ -1,72 +1,14 @@
import React from "react"; import React from "react";
import { View } from "react-native"; import { View } from "react-native";
import { registerTypeDetector, registerIntercept } from "../lib/createElementIntercept"; import { registerTypeDetector, registerIntercept } from "../lib/createElementIntercept";
import { captureFiberRef } from "../lib/fiberCapture";
const TAG = "[ServerDrawer]"; const TAG = "[ServerDrawer]";
// Key Inspector's own fiber capture only fires from a View rendered inside ITS OWN Settings // Whether this capture point actually mounts depends entirely on whether the intercept below
// screen - and Settings very likely renders as a separate native modal/root from MainTabs' guild // fires (unverified independently of it - see lib/fiberCapture.ts for the primary, confirmed
// tab content, so a fiber captured there can never reach anything under the Guilds tab (Quest // capture point inside ServerDrawerSheet.tsx itself, which is guaranteed to mount whenever the
// Dock included) via .return, no matter what screen was visited beforehand. Confirmed live: two // drawer is visibly showing). Kept here too as a second chance, harmless either way.
// independent searches for Quest Dock components both came up empty even while a real quest was
// visibly rendering on the Guilds screen, and a fiber-tree walk from a Settings-screen capture
// couldn't find QuestDockWithQuestContext at all.
//
// GuildsBar's hidden slot is a reliable alternative capture point: it's confirmed to actually
// mount on the Guilds screen (that's this whole file's job), and it's a sibling of the Quest Dock
// in the same tree (both live under MainTabsNavigatorPanel), so walking up from here and back down
// reaches Quest Dock correctly. Stored under the same window.__keyInspectorFiberRoot/
// __keyInspectorFiberSelf globals Key Inspector's Eval scripts already expect, so no existing
// script needs to change - this just gives them a capture point that's actually reachable.
function captureFiberRef(instance: any) {
if (!instance) return;
try {
let fiber: any = null;
if (instance._internalFiberInstanceHandleDEV) {
fiber = instance._internalFiberInstanceHandleDEV;
} else if (instance._internalInstanceHandle) {
fiber = instance._internalInstanceHandle;
} else if (instance.__internalInstanceHandle) {
fiber = instance.__internalInstanceHandle;
} else {
const fiberKey = Object.keys(instance).find(
(k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")
);
if (fiberKey) fiber = instance[fiberKey];
}
if (!fiber) return;
if (fiber.return === undefined && fiber.child === undefined) {
const nested = fiber.stateNode ?? fiber.fiber ?? fiber._debugOwner;
if (nested && (nested.return !== undefined || nested.child !== undefined)) {
fiber = nested;
} else {
return;
}
}
let root = fiber;
let guard = 0;
while (root.return && guard < 5000) {
root = root.return;
guard++;
}
// Dedicated globals, not window.__keyInspectorFiberRoot/__keyInspectorFiberSelf: Key
// Inspector's own Settings screen has its own capture View using those same names, and its
// ref re-fires every time that screen mounts - including just scrolling to the Eval console
// to paste the next script. Sharing the names meant navigating back to Settings silently
// overwrote this capture with one rooted in Settings' own (likely separate-modal) tree,
// which is exactly the failure this capture point exists to avoid. Confirmed live: a text
// dump meant to read GuildsBar's slot came back with Key Inspector's own UI strings instead.
(window as any).__serverDrawerFiberRoot = root;
(window as any).__serverDrawerFiberSelf = fiber;
} catch {
// Best-effort diagnostic capture - a failure here shouldn't affect GuildsBar staying hidden.
}
}
function Nothing() { function Nothing() {
return React.createElement(View, { return React.createElement(View, {
ref: captureFiberRef, ref: captureFiberRef,