ServerDrawer: fix memo-wrapper name detection and decoy-module lookups
This commit is contained in:
parent
259b9b5b31
commit
58ddfc1f2d
13 changed files with 243 additions and 124 deletions
|
|
@ -1,11 +1,17 @@
|
|||
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 "../lib/color";
|
||||
import { lazy } from "../lib/lazy";
|
||||
import { rawFindByValidProps } from "../lib/rawFind";
|
||||
import { getHaptic } from "../lib/commonModules";
|
||||
|
||||
const TextStyleSheet = findByProps("TextStyleSheet")?.TextStyleSheet;
|
||||
const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes");
|
||||
// getHaptic comes from lib/commonModules.ts - see that file for the decoy-module writeup.
|
||||
// TextStyleSheet is object-shaped (a style lookup table), so it needs the same shape-validated
|
||||
// lookup rather than a plain "does this property exist" check.
|
||||
const getTextStyleSheet = lazy(() => rawFindByValidProps<any>({
|
||||
TextStyleSheet: (v) => v?.["text-md/bold"] !== undefined,
|
||||
})?.TextStyleSheet);
|
||||
|
||||
function resolve(token: any, fallback: string): string {
|
||||
return resolveSemanticColor(token) ?? fallback;
|
||||
|
|
@ -44,6 +50,9 @@ export function ContextMenuModal({
|
|||
anchorY: number;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const TextStyleSheet = getTextStyleSheet();
|
||||
const Haptic = getHaptic();
|
||||
|
||||
const { width: winW, height: winH } = Dimensions.get("window");
|
||||
|
||||
const titleH = title ? PAD + 20 + DIVIDER_H : 0;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
import React from "react";
|
||||
import { View, Pressable, Image, StyleSheet } from "react-native";
|
||||
import { findByProps, findByName } from "@vendetta/metro";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { lazy } from "../lib/lazy";
|
||||
import { rawFindByFunctionProps, rawFindByName } from "../lib/rawFind";
|
||||
import { getFlux, getHaptic, getColorModule } from "../lib/commonModules";
|
||||
|
||||
const ICON = 48;
|
||||
|
||||
const ChatIcon = getAssetIDByName("ChatIcon");
|
||||
const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes");
|
||||
const ChannelActions = findByProps("selectPrivateChannel");
|
||||
const SelectedChannelStore = findByName("SelectedChannelStore");
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const GuildStore = findByProps("getGuildId");
|
||||
const colors = findByProps("colors", "unsafe_rawColors")?.colors;
|
||||
|
||||
// getFlux/getHaptic/getColorModule come from lib/commonModules.ts - see that file for the
|
||||
// decoy-module writeup. The rest switch from eager, module-load-time findByProps/findByName (the
|
||||
// caching finders, permanently wrong if they run before the target module registers) to lazy +
|
||||
// raw variants. NavContext is findByProps("getGuildId") - a navigation-context helper, NOT the
|
||||
// real guild data store (that one is reached via findByStoreName elsewhere in this plugin) -
|
||||
// named accordingly here to avoid confusing the two.
|
||||
const getChannelActions = lazy(() => rawFindByFunctionProps<any>("selectPrivateChannel"));
|
||||
const getSelectedChannelStore = lazy(() => rawFindByName<any>("SelectedChannelStore"));
|
||||
const getNavContext = lazy(() => rawFindByFunctionProps<any>("getGuildId"));
|
||||
|
||||
function openDms() {
|
||||
Haptic?.triggerHapticFeedback(Haptic.HapticFeedbackTypes.SOFT);
|
||||
const Haptic = getHaptic();
|
||||
Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.SOFT);
|
||||
const ChannelActions = getChannelActions();
|
||||
if (ChannelActions?.selectPrivateChannel) {
|
||||
const lastChannelId = SelectedChannelStore?.getLastSelectedChannelId?.();
|
||||
const lastChannelId = getSelectedChannelStore()?.getLastSelectedChannelId?.();
|
||||
ChannelActions.selectPrivateChannel(lastChannelId);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,9 +33,13 @@ function openDms() {
|
|||
// moved into the drawer's own grid instead, as the first tile, so DMs live in the same place as
|
||||
// every other server rather than in a leftover spot from the sidebar this plugin otherwise hides.
|
||||
export default function DmTile() {
|
||||
const Flux = getFlux();
|
||||
const NavContext = getNavContext();
|
||||
const colors = getColorModule()?.colors;
|
||||
|
||||
const selected = Flux?.useStateFromStores?.(
|
||||
[GuildStore],
|
||||
() => GuildStore?.getGuildId?.() == null,
|
||||
[NavContext],
|
||||
() => NavContext?.getGuildId?.() == null,
|
||||
) ?? false;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
import React from "react";
|
||||
import { View, Text, Animated, Pressable, Image, ViewStyle, StyleSheet } from "react-native";
|
||||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import { useFolderExpanded, GuildNode } from "../utils/theme";
|
||||
import { lazy } from "../lib/lazy";
|
||||
import { rawFindByFunctionProps, rawFindByStoreName } from "../lib/rawFind";
|
||||
import { getFlux } from "../lib/commonModules";
|
||||
import GuildIcon from "./GuildIcon";
|
||||
import GuildItem from "./GuildItem";
|
||||
|
||||
const GuildActions = findByProps("toggleGuildFolderExpand");
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const GuildReadStateStore = findByStoreName("GuildReadStateStore");
|
||||
// getFlux comes from lib/commonModules.ts - see that file for the decoy-module writeup.
|
||||
// GuildActions/GuildReadStateStore switch from eager, module-load-time findByProps/findByStoreName
|
||||
// (the caching finders) to lazy + raw variants for the same reasons established elsewhere in this
|
||||
// plugin - timing (permanent-negative-cache) for the store, and decoy-avoidance for the function.
|
||||
const getGuildActions = lazy(() => rawFindByFunctionProps<any>("toggleGuildFolderExpand"));
|
||||
const getGuildReadStateStore = lazy(() => rawFindByStoreName("GuildReadStateStore"));
|
||||
|
||||
const ICON = 48;
|
||||
const MINI = 16;
|
||||
|
|
@ -30,6 +35,9 @@ function folderColor(color?: number | null): string {
|
|||
}
|
||||
|
||||
function FolderBadge({ node }: { node: GuildNode }) {
|
||||
const Flux = getFlux();
|
||||
const GuildReadStateStore = getGuildReadStateStore();
|
||||
|
||||
const total = Flux?.useStateFromStores?.(
|
||||
[GuildReadStateStore],
|
||||
() => {
|
||||
|
|
@ -97,7 +105,7 @@ export default function FolderItem({ node, onPick }: { node: GuildNode; onPick:
|
|||
const open = useFolderExpanded(node.id);
|
||||
|
||||
const toggle = () => {
|
||||
GuildActions?.toggleGuildFolderExpand?.(node.id);
|
||||
getGuildActions()?.toggleGuildFolderExpand?.(node.id);
|
||||
};
|
||||
|
||||
if (open) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet } from "react-native";
|
||||
import { findByStoreName, findByProps } from "@vendetta/metro";
|
||||
import { lazy } from "../lib/lazy";
|
||||
import { rawFindByStoreName } from "../lib/rawFind";
|
||||
import { getFlux, getColorModule } from "../lib/commonModules";
|
||||
|
||||
const GuildStore = findByStoreName("GuildStore");
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const colors = findByProps("colors", "unsafe_rawColors")?.colors;
|
||||
// getFlux/getColorModule come from lib/commonModules.ts - see that file for the decoy-module
|
||||
// writeup. GuildStore switches from an eager, module-load-time findByStoreName (the caching
|
||||
// finder, permanently wrong if it runs before the store module has registered) to lazy +
|
||||
// rawFindByStoreName for the same timing reason established elsewhere in this plugin.
|
||||
const getGuildStore = lazy(() => rawFindByStoreName("GuildStore"));
|
||||
|
||||
export default function GuildIcon({ id, size = 48 }: { id: string; size?: number }) {
|
||||
const Flux = getFlux();
|
||||
const GuildStore = getGuildStore();
|
||||
const colors = getColorModule()?.colors;
|
||||
// Guild data streams in progressively after connect, so a one-shot GuildStore.getGuild(id)
|
||||
// taken at mount can miss it entirely and never recover - subscribing means the icon fills in
|
||||
// as soon as its guild finishes loading instead of staying blank for the rest of the session.
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
import React from "react";
|
||||
import { View, Text, Pressable, Animated, StyleSheet } from "react-native";
|
||||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||
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 "../lib/lazy";
|
||||
import { rawFindByProps } from "../lib/rawFind";
|
||||
import { rawFindByStoreName } from "../lib/rawFind";
|
||||
import { getFlux, getHaptic, getBulkAckMod, getReadStateTypesMod } from "../lib/commonModules";
|
||||
import { GuildNode } from "../utils/theme";
|
||||
import GuildIcon from "./GuildIcon";
|
||||
import { ContextMenuModal, ContextMenuItem } from "./ContextMenuModal";
|
||||
|
||||
const ICON = 48;
|
||||
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const GuildReadStateStore = findByStoreName("GuildReadStateStore");
|
||||
const GuildStore = findByStoreName("GuildStore");
|
||||
const GuildChannelStore = findByStoreName("GuildChannelStore");
|
||||
const ChannelStore = findByStoreName("ChannelStore");
|
||||
const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes");
|
||||
// getFlux/getHaptic/getBulkAckMod/getReadStateTypesMod come from lib/commonModules.ts - see that
|
||||
// file for the decoy-module writeup.
|
||||
const getBulkAck = getBulkAckMod;
|
||||
const getReadStateTypes = lazy(() => getReadStateTypesMod()?.ReadStateTypes);
|
||||
|
||||
// rawFindByProps, not findByProps, because these are retried via lazy() - see rawFind.ts for why
|
||||
// a retried cached findByProps call is a no-op after its first failure.
|
||||
const getBulkAck = lazy(() => rawFindByProps("bulkAck", "ackChannel"));
|
||||
const getReadStateTypes = lazy(() => rawFindByProps("ReadStateTypes", "UnreadSetting")?.ReadStateTypes);
|
||||
// These four switch from an eager, module-load-time findByStoreName (the caching finder, permanently
|
||||
// wrong if it runs before the store module has registered) to lazy + rawFindByStoreName for the same
|
||||
// reason established earlier this session, not the decoy-module issue above - unverified whether the
|
||||
// decoy fakes a getName() zero-arg store-name match too, so not claiming that's the cause here.
|
||||
const getGuildReadStateStore = lazy(() => rawFindByStoreName("GuildReadStateStore"));
|
||||
const getGuildStore = lazy(() => rawFindByStoreName("GuildStore"));
|
||||
const getGuildChannelStore = lazy(() => rawFindByStoreName("GuildChannelStore"));
|
||||
const getChannelStore = lazy(() => rawFindByStoreName("ChannelStore"));
|
||||
|
||||
// Discord's own per-guild context menu (getGuildsBarGuildMenuItems) is a bare default export with
|
||||
// no named export - findByName/`.default.name` heuristics for it are unreliable, since a plain
|
||||
|
|
@ -35,6 +37,8 @@ const getReadStateTypes = lazy(() => rawFindByProps("ReadStateTypes", "UnreadSet
|
|||
function markGuildRead(guildId: string) {
|
||||
const bulkAck = getBulkAck();
|
||||
const ReadStateTypes = getReadStateTypes();
|
||||
const GuildChannelStore = getGuildChannelStore();
|
||||
const ChannelStore = getChannelStore();
|
||||
if (!bulkAck?.bulkAck || ReadStateTypes?.CHANNEL == null || !GuildChannelStore?.getSelectableChannelIds) {
|
||||
showToast("Couldn't find Discord's read-state action - this may be unavailable on your version.", undefined);
|
||||
return;
|
||||
|
|
@ -62,6 +66,9 @@ function markGuildRead(guildId: string) {
|
|||
function Badge({ guildId }: { guildId: string }) {
|
||||
useProxy(storage);
|
||||
|
||||
const Flux = getFlux();
|
||||
const GuildReadStateStore = getGuildReadStateStore();
|
||||
|
||||
const mentionCount = Flux?.useStateFromStores?.(
|
||||
[GuildReadStateStore],
|
||||
() => GuildReadStateStore?.getMentionCount?.(guildId) ?? 0,
|
||||
|
|
@ -113,7 +120,7 @@ export default function GuildItem({ node, onPick }: { node: GuildNode; onPick: (
|
|||
|
||||
const showMenu = React.useCallback(() => {
|
||||
const guildId = node.id as string;
|
||||
const guild = GuildStore?.getGuild?.(guildId);
|
||||
const guild = getGuildStore()?.getGuild?.(guildId);
|
||||
if (!guild) return;
|
||||
|
||||
const ref = viewRef.current as any;
|
||||
|
|
@ -142,6 +149,7 @@ export default function GuildItem({ node, onPick }: { node: GuildNode; onPick: (
|
|||
}, [node.id]);
|
||||
|
||||
const handleLongPress = React.useCallback(() => {
|
||||
const Haptic = getHaptic();
|
||||
Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.IMPACT_MEDIUM);
|
||||
showMenu();
|
||||
}, [showMenu]);
|
||||
|
|
|
|||
|
|
@ -1,30 +1,33 @@
|
|||
import React from "react";
|
||||
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 "../lib/lazy";
|
||||
import { rawFindByProps } from "../lib/rawFind";
|
||||
import { rawFindByStoreName } from "../lib/rawFind";
|
||||
import { getHaptic, getBulkAckMod, getReadStateTypesMod } from "../lib/commonModules";
|
||||
|
||||
const ICON = 48;
|
||||
|
||||
const CheckIcon = getAssetIDByName("CheckmarkLargeIcon");
|
||||
const SortedGuildStore = findByStoreName("SortedGuildStore");
|
||||
const GuildChannelStore = findByStoreName("GuildChannelStore");
|
||||
const ChannelStore = findByStoreName("ChannelStore");
|
||||
|
||||
// Lazy + rawFindByProps (not findByProps) because bulkAck lives on an action-creator module that,
|
||||
// like routing, may not be required by Discord's own code yet at the moment this plugin's bundle
|
||||
// loads - and a retried findByProps call is a no-op after its first failure (see rawFind.ts).
|
||||
const getHaptic = lazy(() => rawFindByProps("triggerHapticFeedback", "HapticFeedbackTypes"));
|
||||
const getBulkAck = lazy(() => rawFindByProps("bulkAck", "ackChannel"));
|
||||
const getReadStateTypes = lazy(() => rawFindByProps("ReadStateTypes", "UnreadSetting")?.ReadStateTypes);
|
||||
// getHaptic/getBulkAckMod/getReadStateTypesMod come from lib/commonModules.ts - see that file for
|
||||
// the decoy-module writeup.
|
||||
const getBulkAck = getBulkAckMod;
|
||||
const getReadStateTypes = lazy(() => getReadStateTypesMod()?.ReadStateTypes);
|
||||
|
||||
// These three switch from an eager, module-load-time findByStoreName (the caching finder,
|
||||
// permanently wrong if it runs before the store module has registered) to lazy + rawFindByStoreName
|
||||
// for the same reason established earlier this session, not the decoy-module issue above -
|
||||
// unverified whether the decoy fakes a getName() zero-arg store-name match too.
|
||||
const getSortedGuildStore = lazy(() => rawFindByStoreName("SortedGuildStore"));
|
||||
const getGuildChannelStore = lazy(() => rawFindByStoreName("GuildChannelStore"));
|
||||
const getChannelStore = lazy(() => rawFindByStoreName("ChannelStore"));
|
||||
|
||||
function collectGuildIds(): string[] {
|
||||
// Same tree ServerDrawerSheet already renders from (SortedGuildStore.getGuildsTree()), so this
|
||||
// reaches every guild the drawer shows, folders included, using data already proven to resolve
|
||||
// correctly rather than a separately-shaped GuildStore method.
|
||||
const tree = SortedGuildStore?.getGuildsTree?.();
|
||||
const tree = getSortedGuildStore()?.getGuildsTree?.();
|
||||
const roots = (tree?.root?.children ?? []).filter((n: any) => n?.type !== "root");
|
||||
|
||||
const ids: string[] = [];
|
||||
|
|
@ -46,6 +49,8 @@ function collectGuildIds(): string[] {
|
|||
function markAllServersRead() {
|
||||
const bulkAck = getBulkAck();
|
||||
const ReadStateTypes = getReadStateTypes();
|
||||
const GuildChannelStore = getGuildChannelStore();
|
||||
const ChannelStore = getChannelStore();
|
||||
if (
|
||||
!bulkAck?.bulkAck || ReadStateTypes?.CHANNEL == null ||
|
||||
!GuildChannelStore?.getSelectableChannelIds || !GuildChannelStore?.getVocalChannelIds
|
||||
|
|
|
|||
|
|
@ -1,35 +1,24 @@
|
|||
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 "../lib/lazy";
|
||||
import { rawFind, rawFindByProps } from "../lib/rawFind";
|
||||
import { rawFind, rawFindByFunctionProps } from "../lib/rawFind";
|
||||
import { getFlux, getHaptic, getColorModule } from "../lib/commonModules";
|
||||
import { GuildNode } from "../utils/theme";
|
||||
import GuildItem from "./GuildItem";
|
||||
import FolderItem from "./FolderItem";
|
||||
import DmTile from "./DmTile";
|
||||
import MarkAllReadButton from "./MarkAllReadButton";
|
||||
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const SortedGuildStore = findByStoreName("SortedGuildStore");
|
||||
|
||||
// These are looked up lazily (retried on first real use, not cached the moment this file is
|
||||
// required) because this whole module gets evaluated as soon as the plugin's bundle loads - which
|
||||
// can be before Discord's own routing/navigation modules have been required yet. Crucially, they
|
||||
// use rawFind/rawFindByProps (not @vendetta/metro's cached finders) - Revenge's own finders
|
||||
// permanently cache a "not found" result and never rescan, so retrying a cached findByProps would
|
||||
// just hit that poisoned cache every time instead of ever finding the real module once it
|
||||
// registers (confirmed by reading Revenge's own metro finder source). This is what actually made
|
||||
// tapping a server in the drawer silently do nothing even after adding retries.
|
||||
const getRootNav = lazy(() => rawFindByProps("getRootNavigationRef"));
|
||||
const getHaptic = lazy(() => rawFindByProps("triggerHapticFeedback", "HapticFeedbackTypes"));
|
||||
const getRouting = lazy(() => rawFindByProps("transitionToGuild"));
|
||||
// getFlux/getHaptic/getColorModule come from lib/commonModules.ts - see that file for the decoy
|
||||
// module writeup. These three below are specific to this file only.
|
||||
const getRootNav = lazy(() => rawFindByFunctionProps("getRootNavigationRef"));
|
||||
const getRouting = lazy(() => rawFindByFunctionProps("transitionToGuild"));
|
||||
const getCreateJoinGuildMod = lazy(() => rawFind((m: any) => typeof m?.handleCreateJoinGuildPress === "function"));
|
||||
const getCirclePlusIcon = lazy(() => rawFind((m: any) => m?.CirclePlusIcon)?.CirclePlusIcon);
|
||||
const getRawColors = lazy(() => rawFindByProps("colors", "unsafe_rawColors")?.unsafe_rawColors);
|
||||
|
||||
const ExternalCoordinationMod = find((m: any) => m?.QuestDockExternalCoordinationContext);
|
||||
const ExternalContext = ExternalCoordinationMod?.QuestDockExternalCoordinationContext;
|
||||
const QuestDockMode = find((m: any) => m?.QuestDockMode?.COLLAPSED != null)?.QuestDockMode;
|
||||
const getExternalContext = lazy(() => rawFind((m: any) => m?.QuestDockExternalCoordinationContext)?.QuestDockExternalCoordinationContext);
|
||||
const getQuestDockMode = lazy(() => rawFind((m: any) => m?.QuestDockMode?.COLLAPSED != null)?.QuestDockMode);
|
||||
const getSortedGuildStore = lazy(() => rawFind((m: any) => typeof m?.getName === "function" && m.getName.length === 0 && m.getName() === "SortedGuildStore"));
|
||||
|
||||
const ICON = 48;
|
||||
const GAP = 6;
|
||||
|
|
@ -51,7 +40,7 @@ function CreateJoinButton() {
|
|||
}, []);
|
||||
|
||||
const CirclePlusIcon = getCirclePlusIcon();
|
||||
const rawColors = getRawColors();
|
||||
const rawColors = getColorModule()?.unsafe_rawColors;
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress} onPressIn={scaleDown} onPressOut={scaleUp}>
|
||||
|
|
@ -85,6 +74,9 @@ export default function ServerDrawerSheet({ gestureContext }: { gestureContext:
|
|||
}
|
||||
}, []);
|
||||
|
||||
const Flux = getFlux();
|
||||
const SortedGuildStore = getSortedGuildStore();
|
||||
|
||||
const nodes: GuildNode[] = Flux?.useStateFromStores?.(
|
||||
[SortedGuildStore],
|
||||
() => {
|
||||
|
|
@ -102,12 +94,14 @@ export default function ServerDrawerSheet({ gestureContext }: { gestureContext:
|
|||
if (minH.get() !== h) minH.set(h);
|
||||
}, [minH]);
|
||||
|
||||
const ExternalContext = getExternalContext();
|
||||
const extCtx = ExternalContext ? React.useContext(ExternalContext) as any : null;
|
||||
const setMode = extCtx?.setRestingQuestDockMode;
|
||||
|
||||
const specs = ctx?.questDockWrapperSpecs;
|
||||
|
||||
React.useEffect(() => {
|
||||
const QuestDockMode = getQuestDockMode();
|
||||
if (!setMode || !QuestDockMode || !specs) return;
|
||||
const sub = BackHandler.addEventListener("hardwareBackPress", () => {
|
||||
const h = specs.get()?.height ?? 56;
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
import { find, findByStoreName } from "@vendetta/metro";
|
||||
import { semanticColors } from "@vendetta/ui";
|
||||
import { lazy } from "./lazy";
|
||||
import { rawFind, rawFindByStoreName } from "./rawFind";
|
||||
|
||||
export const HEX_REGEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
||||
|
||||
const ThemeStore = findByStoreName("ThemeStore");
|
||||
// Switched from the caching findByStoreName/find (permanently wrong if either runs before its
|
||||
// target module registers) to lazy + raw variants for the same timing reason established
|
||||
// elsewhere in this plugin.
|
||||
const getThemeStore = lazy(() => rawFindByStoreName<any>("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);
|
||||
const getRawResolveSemanticColor = lazy((): ((theme: any, semanticColor: any) => string | undefined) | undefined =>
|
||||
rawFind((m: any) => m?.default?.internal?.resolveSemanticColor)?.default?.internal?.resolveSemanticColor ??
|
||||
rawFind((m: any) => m?.meta?.resolveSemanticColor)?.meta?.resolveSemanticColor);
|
||||
|
||||
/** 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 {
|
||||
export function resolveSemanticColor(token: any, theme: any = getThemeStore()?.theme): string | undefined {
|
||||
if (!token) return undefined;
|
||||
try {
|
||||
return rawResolveSemanticColor(theme, token);
|
||||
return getRawResolveSemanticColor()?.(theme, token);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -26,7 +29,7 @@ export function resolveSemanticColor(token: any, theme: any = ThemeStore?.theme)
|
|||
* 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 {
|
||||
export function resolveSemanticColorSafe(tokenNames: string[], fallbackHex: string, theme: any = getThemeStore()?.theme): string {
|
||||
for (const name of tokenNames) {
|
||||
const token = (semanticColors as Record<string, any> | undefined)?.[name];
|
||||
if (!token) continue;
|
||||
|
|
|
|||
40
plugins/server-drawer/src/lib/commonModules.ts
Normal file
40
plugins/server-drawer/src/lib/commonModules.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { lazy } from "./lazy";
|
||||
import { rawFindByFunctionProps, rawFindByValidProps } from "./rawFind";
|
||||
|
||||
/**
|
||||
* Confirmed live (Key Inspector's Eval console, full window.modules scan): a single decoy module
|
||||
* matches dozens of common hook/utility property names at once - useStateFromStores,
|
||||
* triggerHapticFeedback, colors, transitionToGuild, bulkAck, and more - all with non-function/
|
||||
* placeholder values (a type-shape/declaration-style object, not real code). Since window.modules
|
||||
* iterates in ascending numeric id order and this decoy has a lower id than every real
|
||||
* implementation it collides with, a plain "does this property exist" check
|
||||
* (findByProps/rawFindByProps) reliably matches the decoy instead of the real thing - every single
|
||||
* time, not occasionally. This is what made tapping a server, marking as read, and haptics all
|
||||
* silently no-op even after this repo's own retry-loop fixes.
|
||||
*
|
||||
* rawFindByFunctionProps requires the matched property to actually be a function, which the decoy's
|
||||
* isn't. Enum/object-shaped properties (which the decoy also fakes) need rawFindByValidProps
|
||||
* instead, checking for a real value the decoy doesn't have.
|
||||
*
|
||||
* Centralized here because every component in this plugin independently needed the same handful of
|
||||
* lookups - keeping one copy means the decoy-avoidance logic can't drift out of sync across files.
|
||||
*/
|
||||
export const getFlux = lazy(() => rawFindByFunctionProps<any>("useStateFromStores"));
|
||||
|
||||
export const getHaptic = lazy(() => rawFindByValidProps<any>({
|
||||
triggerHapticFeedback: (v) => typeof v === "function",
|
||||
HapticFeedbackTypes: (v) => v?.SOFT !== undefined,
|
||||
}));
|
||||
|
||||
/** Has both `.colors` (semantic tokens like TEXT_NORMAL/BG_ACCENT) and `.unsafe_rawColors` (raw palette like WHITE/GREEN_360) on the same module. */
|
||||
export const getColorModule = lazy(() => rawFindByValidProps<any>({
|
||||
colors: (v) => v?.BG_ACCENT !== undefined,
|
||||
unsafe_rawColors: (v) => v?.WHITE !== undefined,
|
||||
}));
|
||||
|
||||
export const getBulkAckMod = lazy(() => rawFindByFunctionProps<any>("bulkAck", "ackChannel"));
|
||||
|
||||
export const getReadStateTypesMod = lazy(() => rawFindByValidProps<any>({
|
||||
ReadStateTypes: (v) => v?.CHANNEL !== undefined,
|
||||
UnreadSetting: (v) => v?.ALL_MESSAGES !== undefined,
|
||||
}));
|
||||
|
|
@ -68,6 +68,27 @@ export function rawFindByFunctionProps<T = any>(...props: string[]): T | undefin
|
|||
return rawFind<T>((m) => props.every((p) => typeof m?.[p] === "function"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Same idea as rawFindByFunctionProps, but for properties whose real value is an object/enum, not
|
||||
* a function (e.g. ReadStateTypes, HapticFeedbackTypes) - "is it a function" can't discriminate
|
||||
* those from the same decoy module rawFindByFunctionProps was built for, since the decoy's version
|
||||
* is *also* an object. Confirmed live that the decoy's enum-shaped properties don't hold the real
|
||||
* values (e.g. its ReadStateTypes/UnreadSetting come back null/empty on direct access, despite
|
||||
* matching a plain "property exists" check) - so this instead requires each named property to pass
|
||||
* its own validator against a value only the real implementation would actually have, e.g.:
|
||||
* rawFindByValidProps({ ReadStateTypes: (v) => v?.CHANNEL !== undefined }).
|
||||
*/
|
||||
export function rawFindByValidProps<T = any>(validators: Record<string, (value: any) => boolean>): T | undefined {
|
||||
const props = Object.keys(validators);
|
||||
return rawFind<T>((m) => props.every((p) => {
|
||||
try {
|
||||
return validators[p](m?.[p]);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
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]";
|
||||
|
||||
// rawFind, not @vendetta/metro's find - both patchExpanded and patchEmpty are retried by index.ts's
|
||||
// fast-retry loop, and Revenge's own find() permanently caches a "not found" result per call site
|
||||
// and never rescans (see rawFind.ts), which would make the retry pointless after the first
|
||||
// failed attempt.
|
||||
let cachedGestureContext: any = null;
|
||||
function getGestureContext(): any {
|
||||
if (!cachedGestureContext) {
|
||||
|
|
@ -17,47 +13,50 @@ function getGestureContext(): any {
|
|||
return cachedGestureContext;
|
||||
}
|
||||
|
||||
export function patchExpanded(
|
||||
cleanups: (() => void)[]
|
||||
): boolean {
|
||||
const mod = rawFind((m) => m?.type?.displayName === "QuestDockContentExpanded" || m?.type?.name === "QuestDockContentExpanded");
|
||||
if (!mod?.type) {
|
||||
console.log(TAG, "WARN: QuestDockContentExpanded not found (will retry)");
|
||||
return false;
|
||||
}
|
||||
const orig = mod.type;
|
||||
// Same fix as hideGuildsBar.tsx: memo-wrapped components are passed to createElement/jsx as the
|
||||
// outer wrapper object, which has no own .name/.displayName - only its nested .type does. Missing
|
||||
// that nested check meant this never matched anything, which is why Discord's real
|
||||
// QuestDockEnrolledHeader (and friends) kept running unpatched against the fake quest data.
|
||||
function isNamed(name: string) {
|
||||
return (type: any) => type?.name === name || type?.displayName === name ||
|
||||
type?.type?.name === name || type?.type?.displayName === name;
|
||||
}
|
||||
|
||||
registerIntercept(orig, ServerDrawerSheet, { gestureContext: getGestureContext() });
|
||||
function Nothing() {
|
||||
return null;
|
||||
}
|
||||
|
||||
mod.type = function ServerDrawerPatch() {
|
||||
return <ServerDrawerSheet gestureContext={getGestureContext()} />;
|
||||
};
|
||||
cleanups.push(() => { mod.type = orig; });
|
||||
console.log(TAG, "PATCH: QuestDockContentExpanded replaced");
|
||||
/**
|
||||
* Reset after live verification (Key Inspector's fiber capture + Eval, same methodology that found
|
||||
* GuildsBar's bug) showed this file's original approach shares the exact same flaw: rawFind matching
|
||||
* a component by `.type.displayName`/`.type.name` across window.modules can - and here, does - match
|
||||
* a different, unrelated module than the one actually mounted. The visible symptom: Discord's real
|
||||
* QuestDockEnrolledHeader (and friends) kept running unpatched, then crashed reading properties off
|
||||
* the fake quest data useMobileQuestDock.ts hands out, because our replacement was silently mutating
|
||||
* the wrong object the whole time.
|
||||
*
|
||||
* registerTypeDetector sidesteps this the same way it does for GuildsBar: it inspects the `type`
|
||||
* argument passed directly to createElement/jsx at the moment something actually creates an element
|
||||
* with it, which is necessarily the real, live reference - there's no module-registry ambiguity
|
||||
* possible. registerIntercept then swaps that exact reference (matched by identity) for the
|
||||
* replacement on every future creation.
|
||||
*/
|
||||
export function patchExpanded(cleanups: (() => void)[]): boolean {
|
||||
registerTypeDetector(isNamed("QuestDockContentExpanded"), (real) => {
|
||||
registerIntercept(real, ServerDrawerSheetWrapper);
|
||||
console.log(TAG, "PATCH: found the real QuestDockContentExpanded, now rendering the drawer");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export function patchEmpty(
|
||||
name: string,
|
||||
cleanups: (() => void)[]
|
||||
): boolean {
|
||||
const mod = rawFind((m) => m?.type?.displayName === name || m?.type?.name === name);
|
||||
if (!mod?.type) {
|
||||
console.log(TAG, `WARN: ${name} not found`);
|
||||
return false;
|
||||
}
|
||||
const orig = mod.type;
|
||||
const originalComponent = orig;
|
||||
function ServerDrawerSheetWrapper() {
|
||||
return <ServerDrawerSheet gestureContext={getGestureContext()} />;
|
||||
}
|
||||
|
||||
mod.type = function EmptyPatch() {
|
||||
return null;
|
||||
};
|
||||
|
||||
if (originalComponent) {
|
||||
Object.defineProperties(mod.type, Object.getOwnPropertyDescriptors(originalComponent));
|
||||
mod.type.displayName = name;
|
||||
}
|
||||
|
||||
cleanups.push(() => { mod.type = orig; });
|
||||
export function patchEmpty(name: string, cleanups: (() => void)[]): boolean {
|
||||
registerTypeDetector(isNamed(name), (real) => {
|
||||
registerIntercept(real, Nothing);
|
||||
console.log(TAG, `PATCH: found the real ${name}, now rendering nothing`);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,14 @@ function Nothing() {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Confirmed live (Key Inspector's fiber capture): the parent creates GuildsBar via its OUTER
|
||||
// React.memo wrapper object ($$typeof: Symbol(react.memo)), which has no own .name/.displayName at
|
||||
// all - those only exist on the memo's inner function, at type.type.name/type.type.displayName.
|
||||
// Checking only the outer two (as the first version of this file did) never matches anything,
|
||||
// which is why the intercept never fired despite the underlying mechanism being sound.
|
||||
function isGuildsBar(type: any): boolean {
|
||||
return type?.name === "GuildsBar" || type?.displayName === "GuildsBar";
|
||||
return type?.name === "GuildsBar" || type?.displayName === "GuildsBar" ||
|
||||
type?.type?.name === "GuildsBar" || type?.type?.displayName === "GuildsBar";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||
import { lazy } from "../lib/lazy";
|
||||
import { rawFindByStoreName } from "../lib/rawFind";
|
||||
import { getFlux, getColorModule } from "../lib/commonModules";
|
||||
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const colors = findByProps("colors", "unsafe_rawColors")?.colors;
|
||||
const ExpandedGuildFolderStore = findByStoreName("ExpandedGuildFolderStore");
|
||||
// getFlux/getColorModule come from lib/commonModules.ts - see that file for the decoy-module
|
||||
// writeup. ExpandedGuildFolderStore switches from an eager, module-load-time findByStoreName (the
|
||||
// caching finder, permanently wrong if it runs before the store module has registered) to lazy +
|
||||
// rawFindByStoreName for the same timing reason established elsewhere in this plugin.
|
||||
const getExpandedGuildFolderStore = lazy(() => rawFindByStoreName("ExpandedGuildFolderStore"));
|
||||
|
||||
export function useTheme() {
|
||||
const colors = getColorModule()?.colors;
|
||||
return {
|
||||
text: colors?.TEXT_NORMAL ?? "#dbdee1",
|
||||
folder: "#5865f2",
|
||||
|
|
@ -13,6 +18,8 @@ export function useTheme() {
|
|||
}
|
||||
|
||||
export function useFolderExpanded(folderId: string | number): boolean {
|
||||
const Flux = getFlux();
|
||||
const ExpandedGuildFolderStore = getExpandedGuildFolderStore();
|
||||
return Flux?.useStateFromStores?.(
|
||||
[ExpandedGuildFolderStore],
|
||||
() => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue