diff --git a/plugins/server-drawer/src/components/GuildItem.tsx b/plugins/server-drawer/src/components/GuildItem.tsx
index c2820ee..9451d6f 100644
--- a/plugins/server-drawer/src/components/GuildItem.tsx
+++ b/plugins/server-drawer/src/components/GuildItem.tsx
@@ -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 (
@@ -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,
diff --git a/plugins/server-drawer/src/components/MarkAllReadButton.tsx b/plugins/server-drawer/src/components/MarkAllReadButton.tsx
deleted file mode 100644
index 44df37a..0000000
--- a/plugins/server-drawer/src/components/MarkAllReadButton.tsx
+++ /dev/null
@@ -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 (
-
-
-
- );
-}
-
-const st = StyleSheet.create({
- outer: { width: ICON, height: ICON, alignItems: "center", justifyContent: "center" },
-});
diff --git a/plugins/server-drawer/src/components/ServerDrawerSheet.tsx b/plugins/server-drawer/src/components/ServerDrawerSheet.tsx
index 01a879f..c6c2ac7 100644
--- a/plugins/server-drawer/src/components/ServerDrawerSheet.tsx
+++ b/plugins/server-drawer/src/components/ServerDrawerSheet.tsx
@@ -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 (
- {/* 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. */}
-
)}
-
);
diff --git a/plugins/server-drawer/src/index.ts b/plugins/server-drawer/src/index.ts
index 7605419..f1014c6 100644
--- a/plugins/server-drawer/src/index.ts
+++ b/plugins/server-drawer/src/index.ts
@@ -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 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;
diff --git a/plugins/server-drawer/src/patches/contentPatch.tsx b/plugins/server-drawer/src/patches/contentPatch.tsx
index 8a12a16..1b32bfb 100644
--- a/plugins/server-drawer/src/patches/contentPatch.tsx
+++ b/plugins/server-drawer/src/patches/contentPatch.tsx
@@ -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 ;
-}
-
-/**
- * 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();
-
- // 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 ;
+ };
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;
diff --git a/plugins/server-drawer/src/ui/Settings.tsx b/plugins/server-drawer/src/ui/Settings.tsx
index 2d725bd..84af8a9 100644
--- a/plugins/server-drawer/src/ui/Settings.tsx
+++ b/plugins/server-drawer/src/ui/Settings.tsx
@@ -15,8 +15,8 @@ export default function Settings() {
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.
- { storage.showUnreadBadges = v; }}
- />