ServerDrawer: rebuild on the original plugin's base, trimmed to requested features
This commit is contained in:
parent
8f845f6540
commit
b094d5d24f
6 changed files with 69 additions and 291 deletions
|
|
@ -1,71 +1,32 @@
|
|||
import React from "react";
|
||||
import { View, Text, Pressable, Animated, StyleSheet } from "react-native";
|
||||
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 { rawFindByStoreName } from "../lib/rawFind";
|
||||
import { getFlux, getHaptic, getBulkAckMod, getReadStateTypesMod } from "../lib/commonModules";
|
||||
import { rawFind, rawFindByStoreName } from "../lib/rawFind";
|
||||
import { getFlux, getHaptic } from "../lib/commonModules";
|
||||
import { GuildNode, useTheme } from "../utils/theme";
|
||||
import GuildIcon from "./GuildIcon";
|
||||
import { ContextMenuModal, ContextMenuItem } from "./ContextMenuModal";
|
||||
|
||||
const ICON = 48;
|
||||
|
||||
// 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);
|
||||
|
||||
// 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"));
|
||||
const getSortedGuildStore = lazy(() => rawFindByStoreName("SortedGuildStore"));
|
||||
|
||||
// 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
|
||||
// function's own .name doesn't survive production minification the way an explicit .displayName
|
||||
// assignment does (confirmed by cross-checking: many memo-wrapped components' .displayName DID
|
||||
// show up as literal strings in decompiled source, this function's .name never does). Rather than
|
||||
// guess at a fragile lookup, this builds a small menu from actions this repo has already confirmed
|
||||
// work elsewhere (the same bulkAck path MarkAllReadButton uses, scoped to one guild).
|
||||
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;
|
||||
}
|
||||
|
||||
const channelIds = [
|
||||
...(GuildChannelStore.getSelectableChannelIds(guildId) ?? []),
|
||||
...(GuildChannelStore.getVocalChannelIds?.(guildId) ?? []),
|
||||
];
|
||||
const channels = channelIds.map((channelId: string) => ({
|
||||
channelId,
|
||||
readStateType: ReadStateTypes.CHANNEL,
|
||||
messageId: ChannelStore?.getChannel?.(channelId)?.lastMessageId ?? null,
|
||||
}));
|
||||
|
||||
if (channels.length === 0) {
|
||||
showToast("Nothing to mark as read.", undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
bulkAck.bulkAck(channels);
|
||||
showToast("Marked as read", undefined);
|
||||
}
|
||||
// no named export - matches the original plugin's own heuristic (a short function whose name
|
||||
// mentions "guild"), kept as-is since it's the only lead either version has ever had for finding
|
||||
// it. Rendered through ContextMenuModal rather than Discord's own native menu component - there's
|
||||
// no accessible way to mount that component standalone outside its usual native modal host - but
|
||||
// every item/action in it is Discord's real menu, not a custom-built one.
|
||||
const getMenuItemsFn = lazy(() =>
|
||||
rawFind((m) => m?.default?.name === "getGuildsBarGuildMenuItems")?.default
|
||||
?? rawFind((m) => typeof m?.default === "function" && m.default.length <= 2 && /guild/i.test(m.default.name ?? ""))?.default
|
||||
);
|
||||
|
||||
function Badge({ guildId }: { guildId: string }) {
|
||||
useProxy(storage);
|
||||
|
||||
const Flux = getFlux();
|
||||
const GuildReadStateStore = getGuildReadStateStore();
|
||||
|
||||
|
|
@ -81,8 +42,6 @@ function Badge({ guildId }: { guildId: string }) {
|
|||
[guildId],
|
||||
) ?? false;
|
||||
|
||||
if (!storage.showUnreadBadges) return null;
|
||||
|
||||
if (mentionCount > 0) {
|
||||
return (
|
||||
<View style={bd.outline}>
|
||||
|
|
@ -111,8 +70,6 @@ function GuildLabel({ guildId }: { guildId: string }) {
|
|||
const Flux = getFlux();
|
||||
const GuildStore = getGuildStore();
|
||||
|
||||
// Same reasoning as GuildIcon: guild data streams in progressively after connect, so this
|
||||
// subscribes rather than reading the name once at mount.
|
||||
const name = Flux?.useStateFromStores?.(
|
||||
[GuildStore],
|
||||
() => GuildStore?.getGuild?.(guildId)?.name ?? "",
|
||||
|
|
@ -145,26 +102,26 @@ export default function GuildItem({ node, onPick }: { node: GuildNode; onPick: (
|
|||
const showMenu = React.useCallback(() => {
|
||||
const guildId = node.id as string;
|
||||
const guild = getGuildStore()?.getGuild?.(guildId);
|
||||
if (!guild) return;
|
||||
const menuItemsFn = getMenuItemsFn();
|
||||
if (!guild || !menuItemsFn) return;
|
||||
|
||||
const treeVersion = getSortedGuildStore()?.getGuildsTree?.()?.version;
|
||||
const rawItems = menuItemsFn(guildId, treeVersion);
|
||||
if (!rawItems?.length) return;
|
||||
|
||||
const ref = viewRef.current as any;
|
||||
if (!ref?.measure) return;
|
||||
|
||||
const items: ContextMenuItem[] = [
|
||||
{ label: "Mark as Read", action: () => markGuildRead(guildId) },
|
||||
{
|
||||
label: "Copy Server ID",
|
||||
action: () => {
|
||||
clipboard.setString(guildId);
|
||||
showToast("Copied server ID", undefined);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
ref.measure((_fx: number, _fy: number, _w: number, _h: number, pageX: number, pageY: number) => {
|
||||
setMenuState({
|
||||
visible: true,
|
||||
items,
|
||||
items: rawItems.map((item: any) => ({
|
||||
label: item.label ?? item.title ?? "Unknown",
|
||||
action: item.action,
|
||||
danger: item.variant === "destructive",
|
||||
iconSource: item.iconSource,
|
||||
IconComponent: item.IconComponent,
|
||||
})),
|
||||
title: guild.name,
|
||||
x: pageX,
|
||||
y: pageY,
|
||||
|
|
|
|||
|
|
@ -1,109 +0,0 @@
|
|||
import React from "react";
|
||||
import { Pressable, Image, StyleSheet } from "react-native";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { showToast } from "@vendetta/ui/toasts";
|
||||
import { lazy } from "../lib/lazy";
|
||||
import { rawFindByStoreName } from "../lib/rawFind";
|
||||
import { getHaptic, getBulkAckMod, getReadStateTypesMod } from "../lib/commonModules";
|
||||
|
||||
const ICON = 48;
|
||||
|
||||
const CheckIcon = getAssetIDByName("CheckmarkLargeIcon");
|
||||
|
||||
// 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 = getSortedGuildStore()?.getGuildsTree?.();
|
||||
const roots = (tree?.root?.children ?? []).filter((n: any) => n?.type !== "root");
|
||||
|
||||
const ids: string[] = [];
|
||||
const visit = (nodes: any[]) => {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "folder") visit(node.children ?? []);
|
||||
else if (node.id != null) ids.push(String(node.id));
|
||||
}
|
||||
};
|
||||
visit(roots);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// Mirrors exactly what Discord's own modules/guild/markGuildsAsRead.tsx does (confirmed against
|
||||
// decompiled current-build source): flatMap getSelectableChannelIds(guild) + getVocalChannelIds
|
||||
// (guild), then bulkAck an array of { channelId, readStateType, messageId }. Built here directly
|
||||
// rather than calling markGuildsAsRead itself, since that module has no named export to reliably
|
||||
// target with findByProps (it's a bare default export with nothing else on the module to key off).
|
||||
function markAllServersRead() {
|
||||
const bulkAck = getBulkAck();
|
||||
const ReadStateTypes = getReadStateTypes();
|
||||
const GuildChannelStore = getGuildChannelStore();
|
||||
const ChannelStore = getChannelStore();
|
||||
if (
|
||||
!bulkAck?.bulkAck || ReadStateTypes?.CHANNEL == null ||
|
||||
!GuildChannelStore?.getSelectableChannelIds || !GuildChannelStore?.getVocalChannelIds
|
||||
) {
|
||||
showToast("Couldn't find Discord's read-state action - this may be unavailable on your version.", undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const guildIds = collectGuildIds();
|
||||
if (guildIds.length === 0) {
|
||||
showToast("Nothing to mark as read.", undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const channels: { channelId: string; readStateType: number; messageId: string | null }[] = [];
|
||||
|
||||
for (const guildId of guildIds) {
|
||||
const channelIds = [
|
||||
...(GuildChannelStore.getSelectableChannelIds(guildId) ?? []),
|
||||
...(GuildChannelStore.getVocalChannelIds(guildId) ?? []),
|
||||
];
|
||||
for (const channelId of channelIds) {
|
||||
channels.push({
|
||||
channelId,
|
||||
readStateType: ReadStateTypes.CHANNEL,
|
||||
messageId: ChannelStore?.getChannel?.(channelId)?.lastMessageId ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (channels.length === 0) {
|
||||
showToast("Nothing to mark as read.", undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
bulkAck.bulkAck(channels);
|
||||
showToast(`Marked ${channels.length} channel${channels.length === 1 ? "" : "s"} as read`, undefined);
|
||||
}
|
||||
|
||||
export default function MarkAllReadButton() {
|
||||
const onPress = React.useCallback(() => {
|
||||
const haptic = getHaptic();
|
||||
haptic?.triggerHapticFeedback?.(haptic.HapticFeedbackTypes.SOFT);
|
||||
markAllServersRead();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress} style={st.outer}>
|
||||
<Image source={CheckIcon} style={{ width: 24, height: 24, tintColor: "#80848e" }} />
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const st = StyleSheet.create({
|
||||
outer: { width: ICON, height: ICON, alignItems: "center", justifyContent: "center" },
|
||||
});
|
||||
|
|
@ -5,12 +5,10 @@ import { useProxy } from "@vendetta/storage";
|
|||
import { lazy } from "../lib/lazy";
|
||||
import { rawFind, rawFindByFunctionProps } from "../lib/rawFind";
|
||||
import { getFlux, getHaptic, getColorModule } from "../lib/commonModules";
|
||||
import { captureFiberRef } from "../lib/fiberCapture";
|
||||
import { GuildNode } from "../utils/theme";
|
||||
import GuildItem from "./GuildItem";
|
||||
import FolderItem from "./FolderItem";
|
||||
import DmTile from "./DmTile";
|
||||
import MarkAllReadButton from "./MarkAllReadButton";
|
||||
|
||||
// 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.
|
||||
|
|
@ -67,12 +65,6 @@ export default function ServerDrawerSheet({ gestureContext }: { gestureContext:
|
|||
|
||||
const routing = getRouting();
|
||||
if (routing?.transitionToGuild) {
|
||||
// Confirmed against decompiled current-build Discord source
|
||||
// (modules/routing/transitionToGuild.native.tsx): the mobile transitionToGuild takes
|
||||
// (guildId, options?) and resolves the channel itself - it does NOT take a channelId
|
||||
// as a second positional argument. Passing one (as this used to) risked being silently
|
||||
// wrong depending on which of the two same-named "transitionToGuild" modules metro
|
||||
// happened to resolve.
|
||||
routing.transitionToGuild(id);
|
||||
} else {
|
||||
getRootNav()?.getRootNavigationRef?.()?.navigate("guilds", { guildId: id });
|
||||
|
|
@ -127,11 +119,6 @@ export default function ServerDrawerSheet({ gestureContext }: { gestureContext:
|
|||
|
||||
return (
|
||||
<View style={st.alignTop}>
|
||||
{/* Invisible - see lib/fiberCapture.ts. This is the reliable capture point: it's
|
||||
guaranteed to mount whenever the drawer itself is visibly rendering, independent of
|
||||
whether hideGuildsBar.tsx's own capture point (which depends on that separate
|
||||
intercept actually firing) works or not. */}
|
||||
<View ref={captureFiberRef} style={{ width: 1, height: 1 }} />
|
||||
<View
|
||||
style={[st.grid, { paddingHorizontal: padX, gap: GAP }]}
|
||||
onLayout={onLayout}
|
||||
|
|
@ -143,7 +130,6 @@ export default function ServerDrawerSheet({ gestureContext }: { gestureContext:
|
|||
: <GuildItem key={node.id} node={node} onPick={pick} />
|
||||
)}
|
||||
<CreateJoinButton />
|
||||
<MarkAllReadButton />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { patchFakeQuestDock } from "./patches/fakeQuestDock";
|
||||
import { patchExpanded, patchEmpty } from "./patches/contentPatch";
|
||||
import { patchQuestDockSlot, patchEmpty } from "./patches/contentPatch";
|
||||
import { patchHideGuildsBar } from "./patches/hideGuildsBar";
|
||||
import { patchCreateElement } from "./lib/createElementIntercept";
|
||||
import { patchAutoCollapseFolders } from "./patches/autoCollapseFolders";
|
||||
|
|
@ -24,17 +24,17 @@ function tryPatch(name: string, fn: () => boolean): boolean {
|
|||
}
|
||||
|
||||
// The Quest Dock (and the internals it depends on) isn't guaranteed to be registered in Metro yet
|
||||
// at the exact moment onLoad runs - same class of startup race fixed elsewhere in this repo
|
||||
// (YouBar+'s button patch, ServerDrawer's own navigation lookups). A single onLoad attempt could
|
||||
// permanently miss one or more of these, which is what "the drawer sometimes just doesn't show up"
|
||||
// looked like. Instead of one shot, this retries whatever hasn't landed yet on a fast interval
|
||||
// until everything's applied (or it gives up after ~10s, which would mean something's genuinely
|
||||
// missing rather than just not loaded yet).
|
||||
// at the exact moment onLoad runs - this is exactly why the original plugin needed a server
|
||||
// tapped before the drawer would show: every one of its lookups was a one-shot find() with no
|
||||
// retry, so a module that hadn't registered yet at that instant stayed unresolved for the rest of
|
||||
// the session. Retrying whatever hasn't landed yet on a fast interval until everything's applied
|
||||
// (or it gives up after ~10s) means the drawer shows up on its own instead of needing a tap to
|
||||
// nudge Discord into registering the modules this plugin is waiting on.
|
||||
function applyAll() {
|
||||
const patchers: Record<string, () => boolean> = {
|
||||
...(storage.fakeQuestDock ? { fakeQuestDock: () => patchFakeQuestDock(cleanups) } : {}),
|
||||
expanded: () => patchExpanded(cleanups),
|
||||
collapsed: () => patchEmpty("QuestDockContentCollapsed", cleanups),
|
||||
expanded: () => patchQuestDockSlot("QuestDockContentExpanded", cleanups),
|
||||
collapsed: () => patchQuestDockSlot("QuestDockContentCollapsed", cleanups),
|
||||
enrolledHeader: () => patchEmpty("QuestDockEnrolledHeader", cleanups),
|
||||
unenrolledHeader: () => patchEmpty("QuestDockUnenrolledHeader", cleanups),
|
||||
enrolledBody: () => patchEmpty("QuestDockEnrolledBody", cleanups),
|
||||
|
|
@ -104,7 +104,7 @@ export function restart() {
|
|||
export default {
|
||||
onLoad: () => {
|
||||
storage.hideGuildsBar ??= true;
|
||||
storage.showUnreadBadges ??= true;
|
||||
storage.showGuildNames ??= true;
|
||||
storage.autoCollapseFolders ??= false;
|
||||
storage.hideFolderIcons ??= false;
|
||||
storage.fakeQuestDock ??= true;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { React } from "@vendetta/metro/common";
|
||||
import { rawFind } from "../lib/rawFind";
|
||||
import ServerDrawerSheet from "../components/ServerDrawerSheet";
|
||||
import { registerIntercept, registerPropsTransform, registerTypeDetector } 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.
|
||||
// rawFind, not @vendetta/metro's find - the original plugin's one-shot find() calls are exactly
|
||||
// why it needed a server tapped before the drawer would show: if the target module hadn't
|
||||
// registered in Metro yet at the exact moment onLoad ran, find()'s result (undefined) is cached
|
||||
// forever and never rescanned, so index.tsx's retry loop calling this again was pointless. rawFind
|
||||
// walks window.modules directly on every call instead, so retrying actually has a chance to work.
|
||||
let cachedGestureContext: any = null;
|
||||
function getGestureContext(): any {
|
||||
if (!cachedGestureContext) {
|
||||
|
|
@ -17,103 +18,53 @@ function getGestureContext(): any {
|
|||
return cachedGestureContext;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
const QUEST_DOCK_NAMES = [
|
||||
"QuestDock",
|
||||
"QuestDockBase",
|
||||
"QuestDockWithQuestContext",
|
||||
"QuestDockMain",
|
||||
"QuestDockContent",
|
||||
"QuestDockExpanded",
|
||||
];
|
||||
|
||||
function isQuestDock(type: any): boolean {
|
||||
return QUEST_DOCK_NAMES.some((name) => isNamed(name)(type));
|
||||
}
|
||||
|
||||
const SD_DRAWER_TEST_ID = "ServerDrawer";
|
||||
|
||||
function hasChildWithTestID(children: any, rest: any[], testID: string): boolean {
|
||||
const inspect = (child: any) => child != null && typeof child === "object" && child.props?.testID === testID;
|
||||
if (children != null) {
|
||||
if (Array.isArray(children)) { if (children.some(inspect)) return true; }
|
||||
else if (inspect(children)) return true;
|
||||
}
|
||||
for (const child of rest) if (inspect(child)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function ServerDrawerSheetWrapper() {
|
||||
return <ServerDrawerSheet gestureContext={getGestureContext()} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
const questDockTypes = new Set<any>();
|
||||
|
||||
// Make the quest dock's wrapper(s) transparent - the ServerDrawerSheet content still renders on
|
||||
// top, but the panel/drawer background that used to be behind the quest menu is removed.
|
||||
registerPropsTransform(
|
||||
(props: any, type: any, rest: any[]) =>
|
||||
questDockTypes.has(type) ||
|
||||
hasChildWithTestID(props?.children, rest, SD_DRAWER_TEST_ID),
|
||||
(props: any) => ({ ...props, style: [props?.style, { backgroundColor: "transparent" }] }),
|
||||
);
|
||||
|
||||
registerTypeDetector("ServerDrawer.QuestDockBg", isQuestDock, (real) => {
|
||||
questDockTypes.add(real);
|
||||
console.log(TAG, "PATCH: QuestDock wrapper set transparent", real?.name ?? real?.displayName);
|
||||
// Patches both QuestDockContentExpanded and QuestDockContentCollapsed to render the drawer, not
|
||||
// just Expanded like the original. Confirmed live (Key Inspector) that restingQuestDockMode - the
|
||||
// Reanimated value Discord uses to pick which of the two to render - starts as "collapsed" on a
|
||||
// fresh load, not "expanded". Patching only Expanded left the drawer invisible behind whatever the
|
||||
// collapsed variant showed (nothing, since that slot went through a separate null-patch) until
|
||||
// something else happened to flip the mode. Swapping in the same drawer for both names sidesteps
|
||||
// needing to fight that mode at all - whichever Discord picks, the drawer is what's there.
|
||||
export function patchQuestDockSlot(name: string, cleanups: (() => void)[]): boolean {
|
||||
registerTypeDetector(`ServerDrawer.Slot.${name}`, isNamed(name), (real) => {
|
||||
registerIntercept(real, ServerDrawerSheet, { gestureContext: getGestureContext() });
|
||||
console.log(TAG, `PATCH: ${name} replaced (type detector)`);
|
||||
}, { persistent: true });
|
||||
|
||||
registerTypeDetector("ServerDrawer.Expanded", isNamed("QuestDockContentExpanded"), (real) => {
|
||||
registerIntercept(real, ServerDrawerSheetWrapper, { testID: SD_DRAWER_TEST_ID });
|
||||
console.log(TAG, "PATCH: QuestDockContentExpanded replaced (type detector)");
|
||||
}, { persistent: true });
|
||||
|
||||
const mod = rawFind((m) => isNamed("QuestDockContentExpanded")(m?.type));
|
||||
const mod = rawFind((m) => isNamed(name)(m?.type));
|
||||
if (!mod?.type) {
|
||||
console.log(TAG, "WARN: QuestDockContentExpanded module not found yet (will retry)");
|
||||
console.log(TAG, `WARN: ${name} not found yet (will retry)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const orig = mod.type;
|
||||
registerIntercept(orig, ServerDrawerSheetWrapper, { testID: SD_DRAWER_TEST_ID });
|
||||
mod.type = ServerDrawerSheetWrapper;
|
||||
registerIntercept(orig, ServerDrawerSheet, { gestureContext: getGestureContext() });
|
||||
mod.type = function ServerDrawerPatch() {
|
||||
return <ServerDrawerSheet gestureContext={getGestureContext()} />;
|
||||
};
|
||||
cleanups.push(() => { mod.type = orig; });
|
||||
console.log(TAG, "PATCH: QuestDockContentExpanded replaced (module mutation)");
|
||||
console.log(TAG, `PATCH: ${name} replaced (module mutation)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sub-sections inside Discord's own dock content (header/body variants) - kept as null-patches
|
||||
// like the original. Once patchQuestDockSlot above wins, Discord's real header/body components
|
||||
// never get created in the first place, so this is mostly moot, but harmless to keep as a backup
|
||||
// for whichever of the two patches loses its own boot race.
|
||||
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 = rawFind((m) => isNamed(name)(m?.type));
|
||||
if (!mod?.type) {
|
||||
console.log(TAG, `WARN: ${name} module not found yet (will retry)`);
|
||||
console.log(TAG, `WARN: ${name} not found yet (will retry)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -126,9 +77,8 @@ export function patchEmpty(name: string, cleanups: (() => void)[]): boolean {
|
|||
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.
|
||||
// Some property descriptors can't be copied onto another function - fine, this is a
|
||||
// best-effort compatibility shim, not something correctness depends on.
|
||||
}
|
||||
mod.type.displayName = name;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ export default function Settings() {
|
|||
<NoteBox>
|
||||
Server Drawer takes over the Quest Dock (the strip above the tab bar) to show your
|
||||
servers as a grid instead - tap to expand, long-press a server for its usual context
|
||||
menu, the chat bubble jumps to DMs, the + button creates or joins a server through
|
||||
Discord's own modal, and the check button marks every channel in every server as read.
|
||||
menu, the chat bubble jumps to DMs, and the + button creates or joins a server
|
||||
through Discord's own modal.
|
||||
</NoteBox>
|
||||
<TableRowGroup title="Layout">
|
||||
<TableSwitchRow
|
||||
|
|
@ -28,12 +28,6 @@ export default function Settings() {
|
|||
restart();
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
label="Unread badges"
|
||||
subLabel="Show mention counts and unread dots on server icons in the drawer"
|
||||
value={storage.showUnreadBadges !== false}
|
||||
onValueChange={(v: boolean) => { storage.showUnreadBadges = v; }}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
label="Show server names"
|
||||
subLabel="Word-wrapped name below each server's icon"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue