diff --git a/plugins/ServerDrawer/manifest.json b/plugins/ServerDrawer/manifest.json
index aea7135..32e11e7 100644
--- a/plugins/ServerDrawer/manifest.json
+++ b/plugins/ServerDrawer/manifest.json
@@ -1,10 +1,13 @@
{
"name": "Server Drawer",
- "description": "Server Drawer above the YouBar, manage your servers differently!",
+ "description": "Server Drawer above the YouBar, manage your servers differently! Now fully hides the Quest Dock/GuildsBar, adds a DM tile, optional server-name labels, and a settings screen.",
"authors": [
{
"name": "kmmiio99o",
"id": "879393496627306587"
+ },
+ {
+ "name": "Rosie"
}
],
"main": "src/index.tsx",
diff --git a/plugins/ServerDrawer/src/components/DmTile.tsx b/plugins/ServerDrawer/src/components/DmTile.tsx
new file mode 100644
index 0000000..8fd97f4
--- /dev/null
+++ b/plugins/ServerDrawer/src/components/DmTile.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+import { View, Pressable, Image, StyleSheet } from "react-native";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { findByProps, findByName } from "@vendetta/metro";
+
+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 NavContext = findByProps("getGuildId");
+const colors = findByProps("colors", "unsafe_rawColors")?.colors;
+
+function openDms() {
+ Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.SOFT);
+ if (ChannelActions?.selectPrivateChannel) {
+ const lastChannelId = SelectedChannelStore?.getLastSelectedChannelId?.();
+ ChannelActions.selectPrivateChannel(lastChannelId);
+ }
+}
+
+// Moved into the drawer's own grid as the first tile, since this plugin otherwise hides the
+// native rail DMs used to live on.
+export default function DmTile() {
+ const selected = Flux?.useStateFromStores?.(
+ [NavContext],
+ () => NavContext?.getGuildId?.() == null,
+ ) ?? false;
+
+ return (
+
+
+
+
+
+ );
+}
+
+const st = StyleSheet.create({
+ outer: { width: ICON, height: ICON },
+ icon: { width: ICON, height: ICON, borderRadius: 16, alignItems: "center", justifyContent: "center" },
+});
diff --git a/plugins/ServerDrawer/src/components/FolderItem.tsx b/plugins/ServerDrawer/src/components/FolderItem.tsx
index 0076189..76088df 100644
--- a/plugins/ServerDrawer/src/components/FolderItem.tsx
+++ b/plugins/ServerDrawer/src/components/FolderItem.tsx
@@ -82,7 +82,7 @@ function FadeIn({ children }: { children: React.ReactNode }) {
return {children};
}
-export default function FolderItem({ node, onPick }: { node: GuildNode; onPick: (id: string) => void }) {
+export default function FolderItem({ node, onPick, showNames }: { node: GuildNode; onPick: (id: string) => void; showNames?: boolean }) {
const open = useFolderExpanded(node.id);
const toggle = () => {
@@ -99,7 +99,7 @@ export default function FolderItem({ node, onPick }: { node: GuildNode; onPick:
{node.children.map((ch) => (
-
+
))}
>
diff --git a/plugins/ServerDrawer/src/components/GuildItem.tsx b/plugins/ServerDrawer/src/components/GuildItem.tsx
index a00ffb8..d3fa502 100644
--- a/plugins/ServerDrawer/src/components/GuildItem.tsx
+++ b/plugins/ServerDrawer/src/components/GuildItem.tsx
@@ -12,6 +12,7 @@ const GuildReadStateStore = findByStoreName("GuildReadStateStore");
const GuildStore = findByStoreName("GuildStore");
const SortedGuildStore = findByStoreName("SortedGuildStore");
const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes");
+const colors = findByProps("colors", "unsafe_rawColors")?.colors;
function Badge({ guildId }: { guildId: string }) {
const mentionCount = Flux?.useStateFromStores?.(
@@ -47,7 +48,7 @@ function Badge({ guildId }: { guildId: string }) {
return null;
}
-export default function GuildItem({ node, onPick }: { node: GuildNode; onPick: (id: string) => void }) {
+export default function GuildItem({ node, onPick, showNames }: { node: GuildNode; onPick: (id: string) => void; showNames?: boolean }) {
const viewRef = React.useRef(null);
const scale = React.useRef(new Animated.Value(1)).current;
const scaleDown = () => Animated.spring(scale, { toValue: 0.85, useNativeDriver: true }).start();
@@ -102,6 +103,12 @@ export default function GuildItem({ node, onPick }: { node: GuildNode; onPick: (
const guildId = node.id as string;
+ const name = Flux?.useStateFromStores?.(
+ [GuildStore],
+ () => GuildStore?.getGuild?.(guildId)?.name ?? "",
+ [guildId],
+ ) ?? "";
+
return (
<>
-
-
-
-
-
+
+
+
+
+
+
+
+ {showNames && (
+
+ {name}
+
+ )}
{
Haptic?.triggerHapticFeedback(Haptic.HapticFeedbackTypes.SOFT);
if (Routing?.transitionToGuild) {
@@ -110,10 +115,11 @@ export default function ServerDrawerSheet({ gestureContext }: { gestureContext:
style={[st.grid, { paddingHorizontal: padX, gap: GAP }]}
onLayout={onLayout}
>
+ {!storage.hideDmTile && }
{nodes.map((node) =>
node.type === "folder"
- ?
- :
+ ?
+ :
)}
diff --git a/plugins/ServerDrawer/src/index.tsx b/plugins/ServerDrawer/src/index.tsx
index d88cde9..27445e3 100644
--- a/plugins/ServerDrawer/src/index.tsx
+++ b/plugins/ServerDrawer/src/index.tsx
@@ -4,13 +4,19 @@ import { patchMobileQuestDock } from "./patches/mobileQuestDock";
import { patchGetQuestAsset } from "./patches/getQuestAsset";
import { patchExpanded, patchEmpty } from "./patches/contentPatch";
import { patchHideGuildsBar } from "./patches/hideGuildsBar";
+import { patchTransparentBackground } from "./patches/transparentBackground";
import { patchCreateElement } from "./patches/createElementIntercept";
+import { storage } from "@vendetta/plugin";
+import Settings from "./ui/Settings";
const TAG = "[ServerDrawer]";
const cleanups: (() => void)[] = [];
export default {
onLoad() {
+ storage.hideDmTile ??= false;
+ storage.showGuildNames ??= false;
+
console.log(TAG, "onLoad");
let patched = 0;
@@ -29,6 +35,7 @@ export default {
if (patchEmpty("QuestDockEnrolledBody", cleanups)) patched++;
if (patchEmpty("QuestDockUnenrolledBody", cleanups)) patched++;
if (patchHideGuildsBar(cleanups)) patched++;
+ if (patchTransparentBackground(cleanups)) patched++;
console.log(TAG, `onLoad done — ${patched} patches applied, ${cleanups.length} cleanups`);
},
@@ -37,4 +44,5 @@ export default {
for (const fn of cleanups) fn();
cleanups.length = 0;
},
+ settings: Settings,
};
diff --git a/plugins/ServerDrawer/src/patches/contentPatch.tsx b/plugins/ServerDrawer/src/patches/contentPatch.tsx
index 735bcad..13c0ec8 100644
--- a/plugins/ServerDrawer/src/patches/contentPatch.tsx
+++ b/plugins/ServerDrawer/src/patches/contentPatch.tsx
@@ -1,7 +1,7 @@
import { find } from "@vendetta/metro";
import { React } from "@vendetta/metro/common";
import ServerDrawerSheet from "../components/ServerDrawerSheet";
-import { registerIntercept } from "./createElementIntercept";
+import { registerIntercept, registerTypeDetector } from "./createElementIntercept";
const TAG = "[ServerDrawer]";
@@ -13,13 +13,26 @@ function getGestureContext(): any {
return cachedGestureContext;
}
+function isNamed(name: string) {
+ return (type: any) => type?.name === name || type?.displayName === name ||
+ type?.type?.name === name || type?.type?.displayName === name;
+}
+
+// find()'s module-registry search can land on a different copy of QuestDockContentExpanded than
+// the one actually mounted - registerTypeDetector instead inspects the exact `type` value passed
+// to createElement/jsx as it's used, so there's no "which copy" ambiguity.
export function patchExpanded(
cleanups: (() => void)[]
): boolean {
+ registerTypeDetector("ServerDrawer.Expanded", isNamed("QuestDockContentExpanded"), (real) => {
+ registerIntercept(real, ServerDrawerSheet, { gestureContext: getGestureContext() });
+ console.log(TAG, "PATCH: QuestDockContentExpanded replaced (type detector)");
+ }, { persistent: true });
+
const mod = find((m) => m?.type?.displayName === "QuestDockContentExpanded" || m?.type?.name === "QuestDockContentExpanded");
if (!mod?.type) {
console.log(TAG, "WARN: QuestDockContentExpanded not found (will retry)");
- return false;
+ return true; // type detector is already watching regardless
}
const orig = mod.type;
@@ -29,7 +42,7 @@ export function patchExpanded(
return ;
};
cleanups.push(() => { mod.type = orig; });
- console.log(TAG, "PATCH: QuestDockContentExpanded replaced");
+ console.log(TAG, "PATCH: QuestDockContentExpanded replaced (module mutation)");
return true;
}
@@ -37,10 +50,15 @@ 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 = find((m) => m?.type?.displayName === name || m?.type?.name === name);
if (!mod?.type) {
console.log(TAG, `WARN: ${name} not found`);
- return false;
+ return true; // type detector is already watching regardless
}
const orig = mod.type;
const originalComponent = orig;
diff --git a/plugins/ServerDrawer/src/patches/createElementIntercept.ts b/plugins/ServerDrawer/src/patches/createElementIntercept.ts
index c803c2d..6c7b5cd 100644
--- a/plugins/ServerDrawer/src/patches/createElementIntercept.ts
+++ b/plugins/ServerDrawer/src/patches/createElementIntercept.ts
@@ -1,56 +1,304 @@
import { React } from "@vendetta/metro/common";
+import { after } from "@vendetta/patcher";
interface Intercept {
replacement: React.ComponentType;
extraProps?: Record;
+ /** How many collapsed-to-zero-size ancestor levels above this element are also allowed to collapse (0 = none). */
+ collapseAncestors?: number;
+}
+
+interface PropsIntercept {
+ predicate: (props: any) => boolean;
+ replacement: React.ComponentType | null;
+}
+
+interface PropsTransform {
+ predicate: (props: any) => boolean;
+ transform: (props: any) => any;
+}
+
+interface TypeDetector {
+ key: string;
+ predicate: (type: any) => boolean;
+ onDetected: (type: any) => void;
+ persistent: boolean;
}
const intercepts = new Map, Intercept>();
-let origCreateElement: typeof React.createElement | null = null;
+const propsIntercepts: PropsIntercept[] = [];
+const propsTransforms: PropsTransform[] = [];
+let typeDetectors: TypeDetector[] = [];
+const detectorKeys = new Set();
let isPatched = false;
+// Elements are created child-first, so a collapse mark on a child is already present by the time
+// the parent's own call is intercepted - that's what makes walking the collapse upward possible.
+const collapseMarks = new WeakMap