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; }