ServerDrawer: make it self-contained, no @shared/lib imports
This commit is contained in:
parent
0b3ddfd6a9
commit
eb6c57b12a
18 changed files with 375 additions and 17 deletions
|
|
@ -2,7 +2,7 @@ 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 "@shared/lib/color";
|
||||
import { resolveSemanticColor } from "../lib/color";
|
||||
|
||||
const TextStyleSheet = findByProps("TextStyleSheet")?.TextStyleSheet;
|
||||
const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes");
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ 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 "@shared/lib/lazy";
|
||||
import { rawFindByProps } from "@shared/lib/rawFind";
|
||||
import { lazy } from "../lib/lazy";
|
||||
import { rawFindByProps } from "../lib/rawFind";
|
||||
import { GuildNode } from "../utils/theme";
|
||||
import GuildIcon from "./GuildIcon";
|
||||
import { ContextMenuModal, ContextMenuItem } from "./ContextMenuModal";
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ 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 "@shared/lib/lazy";
|
||||
import { rawFindByProps } from "@shared/lib/rawFind";
|
||||
import { lazy } from "../lib/lazy";
|
||||
import { rawFindByProps } from "../lib/rawFind";
|
||||
|
||||
const ICON = 48;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
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 "@shared/lib/lazy";
|
||||
import { rawFind, rawFindByProps } from "@shared/lib/rawFind";
|
||||
import { lazy } from "../lib/lazy";
|
||||
import { rawFind, rawFindByProps } from "../lib/rawFind";
|
||||
import { GuildNode } from "../utils/theme";
|
||||
import GuildItem from "./GuildItem";
|
||||
import FolderItem from "./FolderItem";
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { patchMobileQuestDock } from "./patches/mobileQuestDock";
|
|||
import { patchGetQuestAsset } from "./patches/getQuestAsset";
|
||||
import { patchExpanded, patchEmpty } from "./patches/contentPatch";
|
||||
import { patchHideGuildsBar } from "./patches/hideGuildsBar";
|
||||
import { patchCreateElement } from "@shared/lib/createElementIntercept";
|
||||
import { patchCreateElement } from "./lib/createElementIntercept";
|
||||
import { patchAutoCollapseFolders } from "./patches/autoCollapseFolders";
|
||||
import Settings from "./ui/Settings";
|
||||
|
||||
|
|
|
|||
60
plugins/server-drawer/src/lib/color.ts
Normal file
60
plugins/server-drawer/src/lib/color.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { find, findByStoreName } from "@vendetta/metro";
|
||||
import { semanticColors } from "@vendetta/ui";
|
||||
|
||||
export const HEX_REGEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
||||
|
||||
const ThemeStore = findByStoreName("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);
|
||||
|
||||
/** 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 {
|
||||
if (!token) return undefined;
|
||||
try {
|
||||
return rawResolveSemanticColor(theme, token);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as resolveSemanticColor, but tries a list of token names in order and falls back to a
|
||||
* 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 {
|
||||
for (const name of tokenNames) {
|
||||
const token = (semanticColors as Record<string, any> | undefined)?.[name];
|
||||
if (!token) continue;
|
||||
const resolved = resolveSemanticColor(token, theme);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
return fallbackHex;
|
||||
}
|
||||
|
||||
export function isValidHex(value: string | undefined | null): value is string {
|
||||
return !!value && HEX_REGEX.test(value);
|
||||
}
|
||||
|
||||
/** Expands short form #abc to #aabbcc; leaves 6-digit hex untouched. */
|
||||
export function normalizeHex(hex: string): string {
|
||||
if (hex.length === 4) {
|
||||
return "#" + hex.slice(1).split("").map((c) => c + c).join("");
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
export function interpolateColor(color1: string, color2: string, percentage: number): string {
|
||||
const hexToRgb = (hex: string) => (normalizeHex(hex).match(/\w\w/g) ?? ["00", "00", "00"]).map((x) => parseInt(x, 16));
|
||||
const rgbToHex = (rgb: number[]) => "#" + rgb.map((x) => x.toString(16).padStart(2, "0")).join("");
|
||||
|
||||
const rgb1 = hexToRgb(color1);
|
||||
const rgb2 = hexToRgb(color2);
|
||||
const interpolated = rgb1.map((c1, i) => Math.round(c1 + (rgb2[i] - c1) * percentage));
|
||||
|
||||
return rgbToHex(interpolated);
|
||||
}
|
||||
171
plugins/server-drawer/src/lib/createElementIntercept.ts
Normal file
171
plugins/server-drawer/src/lib/createElementIntercept.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { React } from "@vendetta/metro/common";
|
||||
import { rawFind } from "./rawFind";
|
||||
import { waitFor } from "./waitFor";
|
||||
|
||||
interface Intercept {
|
||||
replacement: React.ComponentType<any>;
|
||||
extraProps?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface PropsIntercept {
|
||||
predicate: (props: any) => boolean;
|
||||
replacement: React.ComponentType<any> | null;
|
||||
}
|
||||
|
||||
interface TypeDetector {
|
||||
predicate: (type: any) => boolean;
|
||||
onDetected: (type: any) => void;
|
||||
}
|
||||
|
||||
const intercepts = new Map<React.ComponentType<any>, Intercept>();
|
||||
const propsIntercepts: PropsIntercept[] = [];
|
||||
let typeDetectors: TypeDetector[] = [];
|
||||
let isPatched = false;
|
||||
|
||||
export function registerIntercept(
|
||||
original: React.ComponentType<any>,
|
||||
replacement: React.ComponentType<any>,
|
||||
extraProps?: Record<string, any>,
|
||||
) {
|
||||
intercepts.set(original, { replacement, extraProps });
|
||||
}
|
||||
|
||||
/**
|
||||
* Same idea as registerIntercept, but matches by a predicate over the element's props instead of
|
||||
* an exact type reference - useful when the component itself has no reliable name to find it by
|
||||
* (no named export, and a stripped/mangled runtime function name), but it always passes some
|
||||
* distinguishing literal prop (e.g. a nativeID) that survives into the compiled bundle unchanged.
|
||||
* Pass `replacement: null` to render nothing at all instead of swapping in a component.
|
||||
*/
|
||||
export function registerPropsIntercept(predicate: (props: any) => boolean, replacement: React.ComponentType<any> | null) {
|
||||
propsIntercepts.push({ predicate, replacement });
|
||||
}
|
||||
|
||||
/**
|
||||
* Purely observational, one-shot: the first time an element is created whose `type` matches
|
||||
* `predicate`, calls `onDetected(type)` with the real, live type reference and then stops
|
||||
* watching - it never alters what actually renders.
|
||||
*
|
||||
* This exists for components that can't be found reliably by searching Metro's module registry
|
||||
* after the fact (findByTypeName/rawFindByTypeName), because that races against whenever the
|
||||
* component first mounts - if it mounts before the search finds it, and it never mounts again for
|
||||
* the rest of the session (true for some React.memo'd chrome components), the search losing that
|
||||
* race means the patch never gets a chance to apply at all, permanently. Intercepting element
|
||||
* creation instead sidesteps the race entirely: by the time ANYTHING calls createElement/jsx with
|
||||
* this component as `type`, that reference already exists and is already the real one about to be
|
||||
* used - there's no way to observe the call any earlier than this.
|
||||
*/
|
||||
export function registerTypeDetector(predicate: (type: any) => boolean, onDetected: (type: any) => void) {
|
||||
typeDetectors.push({ predicate, onDetected });
|
||||
}
|
||||
|
||||
function runTypeDetectors(type: any) {
|
||||
if (typeDetectors.length === 0) return;
|
||||
const remaining: TypeDetector[] = [];
|
||||
for (const detector of typeDetectors) {
|
||||
let matched = false;
|
||||
try {
|
||||
matched = detector.predicate(type);
|
||||
} catch {
|
||||
// A bad predicate shouldn't block every other detector or every element from rendering.
|
||||
}
|
||||
if (matched) {
|
||||
try {
|
||||
detector.onDetected(type);
|
||||
} catch {
|
||||
// Same - a detector's own handler throwing shouldn't take anything else down.
|
||||
}
|
||||
} else {
|
||||
remaining.push(detector);
|
||||
}
|
||||
}
|
||||
typeDetectors = remaining;
|
||||
}
|
||||
|
||||
/** Returns a replacement type if this element should be intercepted, `null` to render nothing, or `undefined` to pass through unchanged. */
|
||||
function resolveReplacement(type: any, props: any): { type: any; props: any } | null | undefined {
|
||||
runTypeDetectors(type);
|
||||
|
||||
if (props) {
|
||||
for (const { predicate, replacement } of propsIntercepts) {
|
||||
try {
|
||||
if (predicate(props)) {
|
||||
return replacement ? { type: replacement, props } : null;
|
||||
}
|
||||
} catch {
|
||||
// A bad predicate shouldn't block every other element from rendering.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entry = intercepts.get(type);
|
||||
if (entry) {
|
||||
return { type: entry.replacement, props: entry.extraProps ? { ...props, ...entry.extraProps } : props };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function patchCreateElement(cleanups: (() => void)[]) {
|
||||
if (isPatched) return;
|
||||
|
||||
const origCreateElement = React.createElement;
|
||||
if (!origCreateElement) {
|
||||
console.warn("[ServerDrawer] React.createElement is null, skipping patch");
|
||||
return;
|
||||
}
|
||||
|
||||
const patchedCreateElement = function (type: any, props: any, ...rest: any[]) {
|
||||
const resolved = resolveReplacement(type, props);
|
||||
if (resolved === null) return null;
|
||||
if (resolved) return origCreateElement.call(React, resolved.type, resolved.props, ...rest);
|
||||
return origCreateElement.call(React, type, props, ...rest);
|
||||
};
|
||||
|
||||
Object.assign(patchedCreateElement, origCreateElement);
|
||||
React.createElement = patchedCreateElement as typeof React.createElement;
|
||||
isPatched = true;
|
||||
|
||||
// Modern React/RN builds (Discord's included) compile JSX through the automatic runtime
|
||||
// (jsx/jsxs from react/jsx-runtime) instead of React.createElement - patching only
|
||||
// createElement misses every one of Discord's own render calls entirely. Found by shape
|
||||
// (jsx/jsxs/Fragment together), not by name, for the same reason everything else in this file
|
||||
// avoids name-based lookups.
|
||||
//
|
||||
// The jsx-runtime lookup is retried (not a single one-shot rawFind) because if that module
|
||||
// hasn't been required yet at the exact moment this runs - very early in boot, when a plugin's
|
||||
// onLoad first calls this - jsx/jsxs would never get patched for the rest of the session at
|
||||
// all, silently. Since Discord renders almost everything through jsx/jsxs and not
|
||||
// createElement, that would make every registerTypeDetector/registerPropsIntercept consumer's
|
||||
// patch a near-total no-op whenever that race was lost.
|
||||
const restoreJsx: (() => void)[] = [];
|
||||
const jsxHandle = waitFor(
|
||||
() => rawFind((m: any) => typeof m?.jsx === "function" && typeof m?.jsxs === "function" && "Fragment" in m),
|
||||
(jsxRuntime: any) => {
|
||||
for (const key of ["jsx", "jsxs"] as const) {
|
||||
const orig = jsxRuntime[key];
|
||||
if (typeof orig !== "function") continue;
|
||||
|
||||
jsxRuntime[key] = function (type: any, props: any, ...rest: any[]) {
|
||||
const resolved = resolveReplacement(type, props);
|
||||
if (resolved === null) return null;
|
||||
if (resolved) return orig.call(this, resolved.type, resolved.props, ...rest);
|
||||
return orig.call(this, type, props, ...rest);
|
||||
};
|
||||
restoreJsx.push(() => { jsxRuntime[key] = orig; });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
cleanups.push(() => {
|
||||
jsxHandle.cancel();
|
||||
if (isPatched) {
|
||||
React.createElement = origCreateElement;
|
||||
isPatched = false;
|
||||
}
|
||||
restoreJsx.forEach((fn) => fn());
|
||||
intercepts.clear();
|
||||
propsIntercepts.length = 0;
|
||||
typeDetectors = [];
|
||||
});
|
||||
}
|
||||
24
plugins/server-drawer/src/lib/lazy.ts
Normal file
24
plugins/server-drawer/src/lib/lazy.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Wraps a metro lookup so it isn't run until first actually used, and retries on every call until
|
||||
* it succeeds once (after which the result is cached forever). A plugin's top-level module code
|
||||
* runs as soon as its bundle is required, which can be before Discord's own code has required an
|
||||
* internal module it depends on - a one-shot findByX() at module scope can permanently cache
|
||||
* undefined even though the real module becomes available moments later. Only meant for values
|
||||
* used inside event handlers or plain (non-hook) render logic - wrapping something used as a
|
||||
* React hook (e.g. passed to useContext) in this can violate the Rules of Hooks, since the
|
||||
* resolved-or-not status is now allowed to change between renders of the same component instance.
|
||||
*/
|
||||
export function lazy<T>(resolve: () => T | null | undefined): () => T | undefined {
|
||||
let cached: T | undefined;
|
||||
let resolved = false;
|
||||
|
||||
return () => {
|
||||
if (resolved) return cached;
|
||||
const value = resolve();
|
||||
if (value != null) {
|
||||
cached = value;
|
||||
resolved = true;
|
||||
}
|
||||
return value ?? undefined;
|
||||
};
|
||||
}
|
||||
58
plugins/server-drawer/src/lib/rawFind.ts
Normal file
58
plugins/server-drawer/src/lib/rawFind.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Self-contained on purpose - this plugin does not import from @shared/*. Revenge's own
|
||||
// findByProps/findByName/findByTypeName/findByStoreName (and even the bare `find`) cache their
|
||||
// result per search - including a NEGATIVE result. Once a search comes up empty, it's marked "not
|
||||
// found" and every later call with the same search never rescans window.modules again, even after
|
||||
// the real module registers (confirmed by reading Revenge's own source: metro/internals/modules.ts's
|
||||
// getModules() returns early on a cached NOT_FOUND flag). That makes retry loops pointless for any
|
||||
// search whose first attempt happens to run before the target is registered - every retry after
|
||||
// that first miss hits the same poisoned cache instead of actually looking again.
|
||||
//
|
||||
// This walks window.modules directly to bypass that cache - but critically, it never forces a
|
||||
// module to initialize (never calls window.__r on something that hasn't run yet). Metro only ever
|
||||
// runs a module's factory once and caches whatever it produced forever, so force-evaluating one
|
||||
// before its own dependencies are ready can permanently wedge it into a broken state for the rest
|
||||
// of the session - no amount of retrying fixes that, since the factory never runs again. This only
|
||||
// inspects modules Metro's own isInitialized flag says have already been required by something
|
||||
// else, which is entirely passive and safe to poll repeatedly - it just waits for Discord's own
|
||||
// code to naturally reach the module we want.
|
||||
declare const window: any;
|
||||
|
||||
export function rawFind<T = any>(predicate: (exports: any) => boolean): T | undefined {
|
||||
const modules = window?.modules;
|
||||
if (!modules) return undefined;
|
||||
|
||||
for (const id in modules) {
|
||||
const def = modules[id];
|
||||
if (!def?.isInitialized) continue;
|
||||
|
||||
const exports = def.publicModule?.exports;
|
||||
if (!exports) continue;
|
||||
|
||||
try {
|
||||
if (predicate(exports)) return exports;
|
||||
if (exports.default != null && predicate(exports.default)) return exports.default;
|
||||
} catch {
|
||||
// A predicate throwing on one module's shape shouldn't stop the scan.
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function rawFindByTypeName<T = any>(name: string): T | undefined {
|
||||
return rawFind<T>((m) => m?.name === name || m?.displayName === name || m?.type?.name === name || m?.type?.displayName === name);
|
||||
}
|
||||
|
||||
export function rawFindByName<T = any>(name: string): T | undefined {
|
||||
return rawFind<T>((m) => m?.name === name || m?.displayName === name);
|
||||
}
|
||||
|
||||
export function rawFindByProps<T = any>(...props: string[]): T | undefined {
|
||||
return rawFind<T>((m) => props.every((p) => m?.[p] !== undefined));
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return rawFind<T>((m) => typeof m?.getName === "function" && m.getName.length === 0 && m.getName() === name);
|
||||
}
|
||||
45
plugins/server-drawer/src/lib/waitFor.ts
Normal file
45
plugins/server-drawer/src/lib/waitFor.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
export interface WaitForHandle {
|
||||
/** Stops polling. Safe to call even after `find` has already resolved. */
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls `find` every `intervalMs` (default 100) until it returns a truthy value, then calls
|
||||
* `onFound` with it and stops polling.
|
||||
*
|
||||
* Needed because a plugin's onLoad can run before every Metro module/component it depends on has
|
||||
* actually been required - on a cold app start that's a real race, not an edge case. A one-shot
|
||||
* `findByX(...)` lookup at that exact moment can come back undefined and, without a retry, never
|
||||
* try again for the rest of the session. `waitFor` checks immediately (so there's no added delay
|
||||
* once the target is already registered) and keeps retrying otherwise.
|
||||
*
|
||||
* Returns a handle whose `cancel()` stops the poll - call it from a plugin's unpatch/onUnload so a
|
||||
* pending lookup doesn't keep firing (or apply a patch) after the plugin's been turned off.
|
||||
*/
|
||||
export function waitFor<T>(find: () => T | null | undefined, onFound: (value: T) => void, intervalMs = 100): WaitForHandle {
|
||||
let cancelled = false;
|
||||
let timer: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
const immediate = find();
|
||||
if (immediate) {
|
||||
onFound(immediate);
|
||||
return { cancel() {} };
|
||||
}
|
||||
|
||||
timer = setInterval(() => {
|
||||
if (cancelled) return;
|
||||
const value = find();
|
||||
if (value) {
|
||||
cancelled = true;
|
||||
clearInterval(timer);
|
||||
onFound(value);
|
||||
}
|
||||
}, intervalMs);
|
||||
|
||||
return {
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
if (timer) clearInterval(timer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { FluxDispatcher } from "@vendetta/metro/common";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { rawFindByStoreName } from "@shared/lib/rawFind";
|
||||
import { rawFindByStoreName } from "../lib/rawFind";
|
||||
|
||||
// Adapted from fres621's BetterFolders (autoCollapse.ts) - ported as a ServerDrawer feature
|
||||
// instead of a standalone plugin because BetterFolders' other half (hideIcons) patches a
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { React } from "@vendetta/metro/common";
|
||||
import { rawFind } from "@shared/lib/rawFind";
|
||||
import { rawFind } from "../lib/rawFind";
|
||||
import ServerDrawerSheet from "../components/ServerDrawerSheet";
|
||||
import { registerIntercept } from "@shared/lib/createElementIntercept";
|
||||
import { registerIntercept } from "../lib/createElementIntercept";
|
||||
|
||||
const TAG = "[ServerDrawer]";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { rawFindByProps } from "@shared/lib/rawFind";
|
||||
import { rawFindByProps } from "../lib/rawFind";
|
||||
|
||||
const TAG = "[ServerDrawer]";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { registerPropsIntercept } from "@shared/lib/createElementIntercept";
|
||||
import { registerPropsIntercept } from "../lib/createElementIntercept";
|
||||
|
||||
const TAG = "[ServerDrawer]";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { rawFindByProps } from "@shared/lib/rawFind";
|
||||
import { rawFindByProps } from "../lib/rawFind";
|
||||
|
||||
// rawFindByProps, not findByProps - see questDockRender.ts for why (retried lookup, and
|
||||
// Revenge's own findByProps permanently caches a "not found" result and never rescans).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { rawFindByProps } from "@shared/lib/rawFind";
|
||||
import { rawFindByProps } from "../lib/rawFind";
|
||||
|
||||
// rawFindByProps, not findByProps - see questDockRender.ts for why (retried lookup, and
|
||||
// Revenge's own findByProps permanently caches a "not found" result and never rescans).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { rawFindByProps } from "@shared/lib/rawFind";
|
||||
import { rawFindByProps } from "../lib/rawFind";
|
||||
|
||||
// rawFindByProps, not findByProps - this gets retried by index.ts's fast-retry loop, and
|
||||
// Revenge's own findByProps permanently caches a "not found" result and never rescans (see
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { rawFindByProps } from "@shared/lib/rawFind";
|
||||
import { rawFindByProps } from "../lib/rawFind";
|
||||
|
||||
// A third gate alongside useIsMobileQuestDockRendered/useIsMobileQuestDockRenderedBase, found by
|
||||
// reading the component that actually consumes all three together
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue