diff --git a/plugins/server-drawer/src/patches/contentPatch.tsx b/plugins/server-drawer/src/patches/contentPatch.tsx
index 735bcad..146da63 100644
--- a/plugins/server-drawer/src/patches/contentPatch.tsx
+++ b/plugins/server-drawer/src/patches/contentPatch.tsx
@@ -1,7 +1,7 @@
import { find } from "@vendetta/metro";
import { React } from "@vendetta/metro/common";
import ServerDrawerSheet from "../components/ServerDrawerSheet";
-import { registerIntercept } from "./createElementIntercept";
+import { registerIntercept, registerTypeDetector } from "./createElementIntercept";
const TAG = "[ServerDrawer]";
@@ -13,13 +13,29 @@ function getGestureContext(): any {
return cachedGestureContext;
}
+function isNamed(name: string) {
+ return (type: any) => type?.name === name || type?.displayName === name ||
+ type?.type?.name === name || type?.type?.displayName === name;
+}
+
+// Confirmed live (Key Inspector): find()'s own module-registry search can land on a different copy
+// of QuestDockContentExpanded than the one actually mounted in the live tree - the eager mutation
+// below silently patches the wrong reference while the real one keeps rendering Discord's own
+// (mostly empty) quest content. registerTypeDetector doesn't search the registry at all - it
+// inspects the exact `type` value passed to createElement/jsx the moment something actually
+// creates an element with it, so there's no "which copy" ambiguity to land on wrong.
export function patchExpanded(
cleanups: (() => void)[]
): boolean {
+ registerTypeDetector("ServerDrawer.Expanded", isNamed("QuestDockContentExpanded"), (real) => {
+ registerIntercept(real, ServerDrawerSheet, { gestureContext: getGestureContext() });
+ console.log(TAG, "PATCH: QuestDockContentExpanded replaced (type detector)");
+ }, { persistent: true });
+
const mod = find((m) => m?.type?.displayName === "QuestDockContentExpanded" || m?.type?.name === "QuestDockContentExpanded");
if (!mod?.type) {
console.log(TAG, "WARN: QuestDockContentExpanded not found (will retry)");
- return false;
+ return true; // type detector is already watching regardless
}
const orig = mod.type;
@@ -29,7 +45,7 @@ export function patchExpanded(
return ;
};
cleanups.push(() => { mod.type = orig; });
- console.log(TAG, "PATCH: QuestDockContentExpanded replaced");
+ console.log(TAG, "PATCH: QuestDockContentExpanded replaced (module mutation)");
return true;
}
@@ -37,10 +53,15 @@ 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 = find((m) => m?.type?.displayName === name || m?.type?.name === name);
if (!mod?.type) {
console.log(TAG, `WARN: ${name} not found`);
- return false;
+ return true; // type detector is already watching regardless
}
const orig = mod.type;
const originalComponent = orig;
diff --git a/plugins/server-drawer/src/patches/createElementIntercept.ts b/plugins/server-drawer/src/patches/createElementIntercept.ts
index c803c2d..e6aaa83 100644
--- a/plugins/server-drawer/src/patches/createElementIntercept.ts
+++ b/plugins/server-drawer/src/patches/createElementIntercept.ts
@@ -1,56 +1,346 @@
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>();
-let origCreateElement: typeof React.createElement | null = null;
+const propsIntercepts: PropsIntercept[] = [];
+const propsTransforms: PropsTransform[] = [];
+let typeDetectors: TypeDetector[] = [];
+const detectorKeys = new Set();
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