From 62192d50352f427fb97733eaa045f9a7b1a143f5 Mon Sep 17 00:00:00 2001
From: everestmcarthur <280397636+everestmcarthur@users.noreply.github.com>
Date: Sun, 2 Aug 2026 11:26:01 -0400
Subject: [PATCH] ServerDrawer: make drawer content-swap timing-independent
---
.../src/patches/contentPatch.tsx | 88 +++++++++++++------
1 file changed, 61 insertions(+), 27 deletions(-)
diff --git a/plugins/server-drawer/src/patches/contentPatch.tsx b/plugins/server-drawer/src/patches/contentPatch.tsx
index fbaec3d..06fa6dc 100644
--- a/plugins/server-drawer/src/patches/contentPatch.tsx
+++ b/plugins/server-drawer/src/patches/contentPatch.tsx
@@ -1,7 +1,7 @@
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]";
@@ -17,47 +17,81 @@ function getGestureContext(): any {
return cachedGestureContext;
}
-export function patchExpanded(
- cleanups: (() => void)[]
-): boolean {
- const mod = rawFind((m) => m?.type?.displayName === "QuestDockContentExpanded" || m?.type?.name === "QuestDockContentExpanded");
+// Memo-wrapped components have no own .name/.displayName - only their nested .type does (confirmed
+// live for GuildsBar). Checked in addition to the direct check, not instead of it.
+function isNamed(name: string) {
+ return (type: any) => type?.name === name || type?.displayName === name ||
+ type?.type?.name === name || type?.type?.displayName === name;
+}
+
+function ServerDrawerSheetWrapper() {
+ return ;
+}
+
+/**
+ * Two independent mechanisms, both active. Mutating the found module's own `.type` property
+ * (matched here by isNamed, same nested-memo-aware check GuildsBar's patch uses) makes the drawer
+ * render immediately if the target module happens to already be registered in Metro by the time
+ * this runs. But that registration only happens once Discord's own code actually renders Quest
+ * Dock's expanded content at least once - which itself depends on questDockContext.ts's Context
+ * value force having already taken effect, and on Discord's own render pass actually happening
+ * within this plugin's ~10s retry window. That's a real boot-time race, confirmed live: identical
+ * code gave different results (drawer showing vs not) across genuinely clean reinstalls, pointing
+ * at timing rather than a deterministic bug.
+ *
+ * registerTypeDetector has no such timing dependency - it inspects the `type` argument passed
+ * directly to createElement/jsx at the moment something actually creates an element with it,
+ * whenever that happens, early or late. Combining both means the drawer shows as fast as possible
+ * when the race is won, and still shows correctly even when it's lost.
+ */
+export function patchExpanded(cleanups: (() => void)[]): boolean {
+ registerTypeDetector(isNamed("QuestDockContentExpanded"), (real) => {
+ registerIntercept(real, ServerDrawerSheetWrapper);
+ console.log(TAG, "PATCH: QuestDockContentExpanded replaced (type detector)");
+ });
+
+ const mod = rawFind((m) => isNamed("QuestDockContentExpanded")(m?.type));
if (!mod?.type) {
- console.log(TAG, "WARN: QuestDockContentExpanded not found (will retry)");
+ console.log(TAG, "WARN: QuestDockContentExpanded module not found yet (will retry)");
return false;
}
+
const orig = mod.type;
-
- registerIntercept(orig, ServerDrawerSheet, { gestureContext: getGestureContext() });
-
- mod.type = function ServerDrawerPatch() {
- return ;
- };
+ registerIntercept(orig, ServerDrawerSheetWrapper);
+ mod.type = ServerDrawerSheetWrapper;
cleanups.push(() => { mod.type = orig; });
- console.log(TAG, "PATCH: QuestDockContentExpanded replaced");
+ console.log(TAG, "PATCH: QuestDockContentExpanded replaced (module mutation)");
return true;
}
-export function patchEmpty(
- name: string,
- cleanups: (() => void)[]
-): boolean {
- const mod = rawFind((m) => m?.type?.displayName === name || m?.type?.name === name);
+export function patchEmpty(name: string, cleanups: (() => void)[]): boolean {
+ registerTypeDetector(isNamed(name), (real) => {
+ registerIntercept(real, function EmptyPatch() { return null; });
+ console.log(TAG, `PATCH: ${name} replaced (type detector)`);
+ });
+
+ const mod = rawFind((m) => isNamed(name)(m?.type));
if (!mod?.type) {
- console.log(TAG, `WARN: ${name} not found`);
+ console.log(TAG, `WARN: ${name} module not found yet (will retry)`);
return false;
}
- const orig = mod.type;
- const originalComponent = orig;
- mod.type = function EmptyPatch() {
+ const orig = mod.type;
+ function EmptyPatch() {
return null;
- };
-
- if (originalComponent) {
- Object.defineProperties(mod.type, Object.getOwnPropertyDescriptors(originalComponent));
- mod.type.displayName = name;
}
+ mod.type = EmptyPatch;
+
+ try {
+ Object.defineProperties(mod.type, Object.getOwnPropertyDescriptors(orig));
+ } catch {
+ // Some property descriptors can't be copied onto another function (e.g. non-configurable
+ // ones on certain memo/forwardRef wrappers) - fine, this is a best-effort compatibility
+ // shim, not something correctness depends on.
+ }
+ mod.type.displayName = name;
cleanups.push(() => { mod.type = orig; });
+ console.log(TAG, `PATCH: ${name} replaced (module mutation)`);
return true;
}