diff --git a/plugins/server-drawer/manifest.json b/plugins/server-drawer/manifest.json deleted file mode 100644 index 340d0bd..0000000 --- a/plugins/server-drawer/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "ServerDrawer", - "description": "Replaces the Quest Dock with a server grid drawer above the YouBar - tap to expand, long-press a server for its context menu, plus a one-tap create/join button.", - "authors": [ - { "name": "Rosie" }, - { "name": "kmmiio99o" }, - { "name": "fres" } - ], - "main": "src/index.tsx", - "vendetta": { - "icon": "ServerIcon" - } -} diff --git a/plugins/server-drawer/src/components/ContextMenuModal.tsx b/plugins/server-drawer/src/components/ContextMenuModal.tsx deleted file mode 100644 index 2d19795..0000000 --- a/plugins/server-drawer/src/components/ContextMenuModal.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import React from "react"; -import { View, Text, Pressable, Image, Modal, StyleSheet, Dimensions, Animated } from "react-native"; -import { semanticColors } from "@vendetta/ui"; -import { findByProps, findByStoreName } from "@vendetta/metro"; - -const TextStyleSheet = findByProps("TextStyleSheet")?.TextStyleSheet; -const ThemeStore = findByStoreName("ThemeStore"); -const colorModule = findByProps("colors", "unsafe_rawColors"); -const colorResolver = colorModule?.internal ?? colorModule?.meta; -const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes"); - -function resolve(color: any): string { - const theme = ThemeStore?.theme; - return (color && colorResolver?.resolveSemanticColor(theme, color)) || "#000000"; -} - -const EDGE = 12; -const PAD = 12; -const ITEM_H = 42; -const MIN_W = 220; -const DIVIDER_H = 4; -const RADIUS = 16; -const ICON_SIZE = 20; - -const SPRING_CONFIG = { tension: 200, friction: 20 }; - -export interface ContextMenuItem { - label: string; - action?: () => void; - danger?: boolean; - iconSource?: number; - IconComponent?: React.ComponentType; -} - -export function ContextMenuModal({ - visible, - items, - title, - anchorX, - anchorY, - onClose, -}: { - visible: boolean; - items: ContextMenuItem[]; - title: string; - anchorX: number; - anchorY: number; - onClose: () => void; -}) { - const { width: winW, height: winH } = Dimensions.get("window"); - - const titleH = title ? PAD + 20 + DIVIDER_H : 0; - const menuH = titleH + items.length * ITEM_H; - - let left = anchorX; - let top = anchorY + 48 + 10; - if (left + MIN_W > winW - EDGE) left = winW - MIN_W - EDGE; - if (left < EDGE) left = EDGE; - if (top + menuH > winH - EDGE) top = anchorY - menuH - 10; - if (top < EDGE) top = EDGE; - - const bgContainer = resolve(semanticColors?.BACKGROUND_SURFACE_HIGHEST); - const borderColor = resolve(semanticColors?.BORDER_SUBTLE); - const textColor = resolve(semanticColors?.TEXT_STRONG); - const pressedColor = resolve(semanticColors?.BACKGROUND_MOD_SUBTLE); - - const backdropOpacity = React.useRef(new Animated.Value(0)).current; - const menuScale = React.useRef(new Animated.Value(0.5)).current; - const menuOpacity = React.useRef(new Animated.Value(0)).current; - - const [show, setShow] = React.useState(false); - const closing = React.useRef(false); - - React.useEffect(() => { - if (visible && !closing.current) { - setShow(true); - backdropOpacity.setValue(0); - menuScale.setValue(0.5); - menuOpacity.setValue(0); - Animated.parallel([ - Animated.timing(backdropOpacity, { toValue: 1, duration: 150, useNativeDriver: true }), - Animated.spring(menuScale, { toValue: 1, useNativeDriver: true, ...SPRING_CONFIG }), - Animated.spring(menuOpacity, { toValue: 1, useNativeDriver: true, ...SPRING_CONFIG }), - ]).start(); - } - }, [visible]); - - const handleClose = React.useCallback(() => { - if (closing.current) return; - closing.current = true; - Animated.parallel([ - Animated.timing(backdropOpacity, { toValue: 0, duration: 120, useNativeDriver: true }), - Animated.timing(menuScale, { toValue: 0.8, duration: 120, useNativeDriver: true }), - Animated.timing(menuOpacity, { toValue: 0, duration: 120, useNativeDriver: true }), - ]).start(() => { - closing.current = false; - setShow(false); - onClose(); - }); - }, [onClose]); - - if (!show) return null; - - return ( - - - - - {title ? ( - - - {title} - - - - ) : null} - {items.map((item, i) => { - const isFirst = i === 0 && !title; - const isLast = i === items.length - 1; - const hasIcon = !!(item.iconSource || item.IconComponent); - - return ( - ({ - paddingHorizontal: PAD, - paddingVertical: 10, - minHeight: ITEM_H, - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - gap: 8, - backgroundColor: pressed ? pressedColor : "transparent", - borderTopLeftRadius: isFirst ? RADIUS : 0, - borderTopRightRadius: isFirst ? RADIUS : 0, - borderBottomLeftRadius: isLast ? RADIUS : 0, - borderBottomRightRadius: isLast ? RADIUS : 0, - borderBottomWidth: !isFirst && !isLast ? 1 : 0, - borderBottomColor: borderColor, - })} - onPress={() => { - Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.IMPACT_LIGHT); - handleClose(); - item.action?.(); - }} - > - - {item.label} - - {hasIcon ? ( - - {item.IconComponent ? ( - - ) : item.iconSource ? ( - - ) : null} - - ) : null} - - ); - })} - - - - ); -} - -const localStyles = StyleSheet.create({ - root: { - flex: 1, - }, - backdrop: { - ...StyleSheet.absoluteFill, - backgroundColor: "rgba(0,0,0,0.55)", - }, - container: { - position: "absolute", - borderWidth: 1, - borderRadius: RADIUS, - overflow: "hidden", - elevation: 20, - shadowColor: "#000", - shadowOffset: { width: 0, height: 12 }, - shadowOpacity: 0.24, - shadowRadius: 24, - }, -}); diff --git a/plugins/server-drawer/src/components/DmTile.tsx b/plugins/server-drawer/src/components/DmTile.tsx deleted file mode 100644 index 8fd97f4..0000000 --- a/plugins/server-drawer/src/components/DmTile.tsx +++ /dev/null @@ -1,44 +0,0 @@ -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/server-drawer/src/components/FolderItem.tsx b/plugins/server-drawer/src/components/FolderItem.tsx deleted file mode 100644 index 76088df..0000000 --- a/plugins/server-drawer/src/components/FolderItem.tsx +++ /dev/null @@ -1,154 +0,0 @@ -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 { useFolderExpanded, GuildNode } from "../utils/theme"; -import GuildIcon from "./GuildIcon"; -import GuildItem from "./GuildItem"; - -const GuildActions = findByProps("toggleGuildFolderExpand"); -const Flux = findByProps("useStateFromStores"); -const GuildReadStateStore = findByStoreName("GuildReadStateStore"); - -const ICON = 48; -const MINI = 16; - -const POS: ViewStyle[] = [ - { top: 6, left: 6 }, - { top: 6, right: 6 }, - { bottom: 6, left: 6 }, - { bottom: 6, right: 6 }, -]; - -const FOLDER_ASSET = getAssetIDByName("FolderIcon"); - -function folderColor(color?: number | null): string { - if (color == null) return "#5865f2"; - return `#${color.toString(16).padStart(6, "0")}`; -} - -function FolderBadge({ node }: { node: GuildNode }) { - const total = Flux?.useStateFromStores?.( - [GuildReadStateStore], - () => { - let sum = 0; - for (const child of node.children) { - sum += GuildReadStateStore?.getMentionCount?.(child.id) ?? 0; - } - return sum; - }, - [node.children], - ) ?? 0; - - if (total > 0) { - return ( - - - {total > 99 ? "99+" : String(total)} - - - ); - } - return null; -} - -function FolderCover({ node }: { node: GuildNode }) { - const col = folderColor(node.color); - return ( - - - {node.children.slice(0, 4).map((ch, i) => ( - - - - ))} - - - - ); -} - -const fc = StyleSheet.create({ - outer: { width: ICON, height: ICON }, - icon: { width: ICON, height: ICON, borderRadius: 16, overflow: "hidden" }, - cell: { position: "absolute", width: MINI, height: MINI, borderRadius: 8, overflow: "hidden" }, -}); - -function FadeIn({ children }: { children: React.ReactNode }) { - const opacity = React.useRef(new Animated.Value(0)).current; - React.useEffect(() => { - Animated.timing(opacity, { toValue: 1, duration: 180, useNativeDriver: true }).start(); - }, []); - return {children}; -} - -export default function FolderItem({ node, onPick, showNames }: { node: GuildNode; onPick: (id: string) => void; showNames?: boolean }) { - const open = useFolderExpanded(node.id); - - const toggle = () => { - GuildActions?.toggleGuildFolderExpand?.(node.id); - }; - - if (open) { - return ( - <> - - - - - - {node.children.map((ch) => ( - - - - ))} - - ); - } - - return ( - - - - ); -} - -const fo = StyleSheet.create({ - openIcon: { - width: ICON, - height: ICON, - borderRadius: 16, - alignItems: "center", - justifyContent: "center", - }, - folderImg: { width: 24, height: 24 }, -}); - -const fbd = StyleSheet.create({ - outline: { - position: "absolute", - bottom: -3, - right: -3, - minWidth: 23, - minHeight: 23, - borderRadius: 12, - backgroundColor: "#1a1a2e", - alignItems: "center", - justifyContent: "center", - }, - badge: { - minWidth: 19, - height: 19, - borderRadius: 9, - backgroundColor: "#ed4245", - alignItems: "center", - justifyContent: "center", - paddingHorizontal: 5, - }, - text: { - color: "#fff", - fontSize: 10, - fontWeight: "700", - lineHeight: 19, - }, -}); diff --git a/plugins/server-drawer/src/components/GuildIcon.tsx b/plugins/server-drawer/src/components/GuildIcon.tsx deleted file mode 100644 index 9aaf6af..0000000 --- a/plugins/server-drawer/src/components/GuildIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import { View, Text, Image, StyleSheet } from "react-native"; -import { findByStoreName, findByProps } from "@vendetta/metro"; - -const GuildStore = findByStoreName("GuildStore"); -const colors = findByProps("colors", "unsafe_rawColors")?.colors; - -export default function GuildIcon({ id, size = 48 }: { id: string; size?: number }) { - const g = GuildStore?.getGuild(id); - if (!g) return null; - const rad = size >= 40 ? 16 : 8; - if (g.icon) { - return ( - - ); - } - return ( - - - {g.name?.split(" ").map((w: string) => w[0]).join("").slice(0, 2).toUpperCase() || "?"} - - - ); -} - -const st = StyleSheet.create({ - fallback: { alignItems: "center", justifyContent: "center" }, - letter: { color: "#fff", fontWeight: "700" }, -}); diff --git a/plugins/server-drawer/src/components/GuildItem.tsx b/plugins/server-drawer/src/components/GuildItem.tsx deleted file mode 100644 index d3fa502..0000000 --- a/plugins/server-drawer/src/components/GuildItem.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import React from "react"; -import { View, Text, Pressable, Animated, StyleSheet } from "react-native"; -import { find, findByProps, findByStoreName } from "@vendetta/metro"; -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 SortedGuildStore = findByStoreName("SortedGuildStore"); -const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes"); -const colors = findByProps("colors", "unsafe_rawColors")?.colors; - -function Badge({ guildId }: { guildId: string }) { - const mentionCount = Flux?.useStateFromStores?.( - [GuildReadStateStore], - () => GuildReadStateStore?.getMentionCount?.(guildId) ?? 0, - [guildId], - ) ?? 0; - - const hasUnread = Flux?.useStateFromStores?.( - [GuildReadStateStore], - () => GuildReadStateStore?.hasUnread?.(guildId) ?? false, - [guildId], - ) ?? false; - - if (mentionCount > 0) { - return ( - - - {mentionCount > 99 ? "99+" : String(mentionCount)} - - - ); - } - - if (hasUnread) { - return ( - - - - ); - } - - return null; -} - -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(); - const scaleUp = () => Animated.spring(scale, { toValue: 1, useNativeDriver: true }).start(); - - const [menuState, setMenuState] = React.useState<{ - visible: boolean; - items: ContextMenuItem[]; - title: string; - x: number; - y: number; - }>({ visible: false, items: [], title: "", x: 0, y: 0 }); - - const showMenu = React.useCallback(() => { - const guildId = node.id as string; - const guild = GuildStore?.getGuild?.(guildId); - const menuItemsFn = find((m) => m?.default?.name === "getGuildsBarGuildMenuItems")?.default - ?? find((m) => typeof m?.default === "function" && m.default.length <= 2 && /guild/i.test(m.default.name ?? ""))?.default; - - if (!guild || !menuItemsFn) { - return; - } - - const treeVersion = SortedGuildStore?.getGuildsTree?.()?.version; - const rawItems = menuItemsFn(guildId, treeVersion); - if (!rawItems?.length) return; - - const ref = viewRef.current as any; - if (!ref?.measure) return; - - ref.measure((_fx: number, _fy: number, _w: number, _h: number, pageX: number, pageY: number) => { - setMenuState({ - visible: true, - 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, - }); - }); - }, [node.id]); - - const handleLongPress = React.useCallback(() => { - Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.IMPACT_MEDIUM); - showMenu(); - }, [showMenu]); - - const guildId = node.id as string; - - const name = Flux?.useStateFromStores?.( - [GuildStore], - () => GuildStore?.getGuild?.(guildId)?.name ?? "", - [guildId], - ) ?? ""; - - return ( - <> - onPick(guildId)} - onLongPress={handleLongPress} - delayLongPress={500} - > - - - - - - - - {showNames && ( - - {name} - - )} - - - setMenuState((s) => ({ ...s, visible: false }))} - /> - - ); -} - -const st = StyleSheet.create({ - outer: { width: ICON, alignItems: "center" }, - iconWrap: { width: ICON, height: ICON }, - icon: { width: ICON, height: ICON, borderRadius: 16, overflow: "hidden" }, - label: { - marginTop: 4, - width: ICON, - fontSize: 10, - lineHeight: 12, - fontWeight: "600", - textAlign: "center", - color: colors?.HEADER_PRIMARY ?? colors?.TEXT_NORMAL ?? "#fff", - textShadowColor: "rgba(0,0,0,0.75)", - textShadowOffset: { width: 0, height: 1 }, - textShadowRadius: 2, - }, -}); - -const bd = StyleSheet.create({ - outline: { - position: "absolute", - bottom: -3, - right: -3, - minWidth: 23, - minHeight: 23, - borderRadius: 12, - backgroundColor: "#1a1a2e", - alignItems: "center", - justifyContent: "center", - }, - badge: { - minWidth: 19, - height: 19, - borderRadius: 9, - backgroundColor: "#ed4245", - alignItems: "center", - justifyContent: "center", - paddingHorizontal: 5, - }, - text: { - color: "#fff", - fontSize: 10, - fontWeight: "700", - lineHeight: 19, - }, - dotOutline: { - position: "absolute", - bottom: -2, - right: -2, - width: 14, - height: 14, - borderRadius: 7, - backgroundColor: "#1a1a2e", - alignItems: "center", - justifyContent: "center", - }, - dot: { - width: 10, - height: 10, - borderRadius: 5, - backgroundColor: "#ed4245", - }, -}); diff --git a/plugins/server-drawer/src/components/ServerDrawerSheet.tsx b/plugins/server-drawer/src/components/ServerDrawerSheet.tsx deleted file mode 100644 index c3af844..0000000 --- a/plugins/server-drawer/src/components/ServerDrawerSheet.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import React from "react"; -import { View, Text, Pressable, Animated, Dimensions, StyleSheet, BackHandler } from "react-native"; -import { find, findByProps, findByStoreName } from "@vendetta/metro"; -import { storage } from "@vendetta/plugin"; -import { useProxy } from "@vendetta/storage"; -import { GuildNode } from "../utils/theme"; -import GuildItem from "./GuildItem"; -import FolderItem from "./FolderItem"; -import DmTile from "./DmTile"; - -const Flux = findByProps("useStateFromStores"); -const SortedGuildStore = findByStoreName("SortedGuildStore"); -const RootNav = findByProps("getRootNavigationRef"); -const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes"); -const Routing = findByProps("transitionToGuild"); - -const CreateJoinGuildMod = find((m: any) => typeof m?.handleCreateJoinGuildPress === "function"); -const CirclePlusIcon = find((m: any) => m?.CirclePlusIcon)?.CirclePlusIcon; -const rawColors = findByProps("colors", "unsafe_rawColors")?.unsafe_rawColors; - -const createJoinBg = rawColors?.GREEN_360; -const createJoinIconColor = rawColors?.WHITE; - -const ExternalCoordinationMod = find((m: any) => m?.QuestDockExternalCoordinationContext); -const ExternalContext = ExternalCoordinationMod?.QuestDockExternalCoordinationContext; -const QuestDockMode = find((m: any) => m?.QuestDockMode?.COLLAPSED != null)?.QuestDockMode; - -const ICON = 48; -const GAP = 6; -const PAD = 12; - -function CreateJoinButton() { - const scale = React.useRef(new Animated.Value(1)).current; - const scaleDown = React.useCallback(() => { - Animated.spring(scale, { toValue: 0.9, useNativeDriver: true }).start(); - }, [scale]); - const scaleUp = React.useCallback(() => { - Animated.spring(scale, { toValue: 1, useNativeDriver: true }).start(); - }, [scale]); - - const onPress = React.useCallback(() => { - Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.SOFT); - CreateJoinGuildMod?.handleCreateJoinGuildPress?.(); - }, []); - - return ( - - - {CirclePlusIcon ? ( - - ) : ( - {"+"} - )} - - - ); -} - -export default function ServerDrawerSheet({ gestureContext }: { gestureContext: any }) { - useProxy(storage); - - const pick = React.useCallback((id: string) => { - Haptic?.triggerHapticFeedback(Haptic.HapticFeedbackTypes.SOFT); - if (Routing?.transitionToGuild) { - Routing.transitionToGuild(id, null); - } else { - RootNav?.getRootNavigationRef()?.navigate("guilds", { guildId: id }); - } - }, []); - - const nodes: GuildNode[] = Flux?.useStateFromStores?.( - [SortedGuildStore], - () => { - const t = SortedGuildStore?.getGuildsTree(); - return (t?.root?.children || []).filter((n: GuildNode) => n.type !== "root"); - }, - ) ?? []; - - const ctx = (gestureContext ? React.useContext(gestureContext) : null) as any; - const minH = ctx?.minExpandedContentHeight; - - const onLayout = React.useCallback((e: any) => { - if (!minH) return; - const h = e.nativeEvent.layout.height; - if (minH.get() !== h) minH.set(h); - }, [minH]); - - const extCtx = ExternalContext ? React.useContext(ExternalContext) as any : null; - const setMode = extCtx?.setRestingQuestDockMode; - - const specs = ctx?.questDockWrapperSpecs; - - React.useEffect(() => { - if (!setMode || !QuestDockMode || !specs) return; - const sub = BackHandler.addEventListener("hardwareBackPress", () => { - const h = specs.get()?.height ?? 56; - if (h > 80) { - setMode(QuestDockMode.COLLAPSED); - return true; - } - return false; - }); - return () => sub.remove(); - }, [setMode, specs]); - - const { width: winW } = Dimensions.get("window"); - - const cols = Math.max(3, Math.floor((winW - PAD * 2 + GAP) / (ICON + GAP))); - const totalW = cols * ICON + (cols - 1) * GAP; - const padX = Math.max(0, (winW - totalW) / 2); - - return ( - - - {!storage.hideDmTile && } - {nodes.map((node) => - node.type === "folder" - ? - : - )} - - - - ); -} - -const st = StyleSheet.create({ - alignTop: { - flex: 1, - justifyContent: "flex-start", - alignItems: "flex-start", - }, - grid: { - flexDirection: "row", - flexWrap: "wrap", - paddingTop: 4, - paddingBottom: 16, - }, - createJoin: { - width: ICON, - height: ICON, - borderRadius: 16, - backgroundColor: createJoinBg, - alignItems: "center", - justifyContent: "center", - }, - createJoinFallback: { - color: createJoinIconColor, - fontSize: 28, - fontWeight: "700", - lineHeight: 30, - }, -}); diff --git a/plugins/server-drawer/src/index.tsx b/plugins/server-drawer/src/index.tsx deleted file mode 100644 index 27445e3..0000000 --- a/plugins/server-drawer/src/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { patchQuestDockRender } from "./patches/questDockRender"; -import { patchQuestDockBase } from "./patches/questDockBase"; -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; - - patchCreateElement(cleanups); - patched++; - - if (patchQuestDockRender(cleanups)) patched++; - if (patchQuestDockBase(cleanups)) patched++; - if (patchMobileQuestDock(cleanups)) patched++; - if (patchGetQuestAsset(cleanups)) patched++; - if (patchExpanded(cleanups)) patched++; - if (patchEmpty("QuestDockContentCollapsed", cleanups)) patched++; - if (patchEmpty("QuestDockEnrolledHeader", cleanups)) patched++; - if (patchEmpty("QuestDockUnenrolledHeader", cleanups)) patched++; - 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`); - }, - onUnload() { - console.log(TAG, "onUnload"); - for (const fn of cleanups) fn(); - cleanups.length = 0; - }, - settings: Settings, -}; diff --git a/plugins/server-drawer/src/patches/contentPatch.tsx b/plugins/server-drawer/src/patches/contentPatch.tsx deleted file mode 100644 index 13c0ec8..0000000 --- a/plugins/server-drawer/src/patches/contentPatch.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { find } from "@vendetta/metro"; -import { React } from "@vendetta/metro/common"; -import ServerDrawerSheet from "../components/ServerDrawerSheet"; -import { registerIntercept, registerTypeDetector } from "./createElementIntercept"; - -const TAG = "[ServerDrawer]"; - -let cachedGestureContext: any = null; -function getGestureContext(): any { - if (!cachedGestureContext) { - cachedGestureContext = find((m) => m?.QuestDockGestureContext)?.QuestDockGestureContext ?? null; - } - 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 true; // type detector is already watching regardless - } - const orig = mod.type; - - registerIntercept(orig, ServerDrawerSheet, { gestureContext: getGestureContext() }); - - mod.type = function ServerDrawerPatch() { - return ; - }; - cleanups.push(() => { mod.type = orig; }); - console.log(TAG, "PATCH: QuestDockContentExpanded replaced (module mutation)"); - return true; -} - -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 true; // type detector is already watching regardless - } - const orig = mod.type; - const originalComponent = orig; - - mod.type = function EmptyPatch() { - return null; - }; - - if (originalComponent) { - Object.defineProperties(mod.type, Object.getOwnPropertyDescriptors(originalComponent)); - mod.type.displayName = name; - } - - cleanups.push(() => { mod.type = orig; }); - return true; -} diff --git a/plugins/server-drawer/src/patches/createElementIntercept.ts b/plugins/server-drawer/src/patches/createElementIntercept.ts deleted file mode 100644 index 6c7b5cd..0000000 --- a/plugins/server-drawer/src/patches/createElementIntercept.ts +++ /dev/null @@ -1,304 +0,0 @@ -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>(); -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(); - -// display: none alone doesn't reclaim space on every layout, so this also zeroes width/flex/margin -// and hides overflow. -const COLLAPSE_STYLE = { - display: "none" as const, - width: 0, - minWidth: 0, - maxWidth: 0, - flexGrow: 0, - flexShrink: 0, - flexBasis: 0, - margin: 0, - padding: 0, - borderWidth: 0, - overflow: "hidden" as const, -}; - -export function registerIntercept( - original: React.ComponentType, - replacement: React.ComponentType, - extraProps?: Record, - options?: { collapseAncestors?: number }, -) { - intercepts.set(original, { replacement, extraProps, collapseAncestors: options?.collapseAncestors }); -} - -// Matches on a props predicate instead of an exact type reference, for components with no -// reliable name. Pass replacement: null to render nothing. -export function registerPropsIntercept(predicate: (props: any) => boolean, replacement: React.ComponentType | null) { - propsIntercepts.push({ predicate, replacement }); -} - -// Rewrites props in place, keeping the same type. -export function registerPropsTransform(predicate: (props: any) => boolean, transform: (props: any) => any) { - propsTransforms.push({ predicate, transform }); -} - -// Fires onDetected(type) the first time a matching element is created. For components with no -// stable module export to search for after the fact. persistent keeps firing on every match -// instead of once, for references that change across navigation. -export function registerTypeDetector( - key: string, - predicate: (type: any) => boolean, - onDetected: (type: any) => void, - options?: { persistent?: boolean }, -) { - if (detectorKeys.has(key)) return; - detectorKeys.add(key); - typeDetectors.push({ key, predicate, onDetected, persistent: options?.persistent ?? false }); -} - -export function hasTypeDetector(key: string): boolean { - return detectorKeys.has(key); -} - -function runTypeDetectors(type: any) { - if (typeDetectors.length === 0) return; - - let consumed = false; - for (const detector of typeDetectors) { - let matched = false; - try { - matched = detector.predicate(type); - } catch { - // Ignore - } - if (!matched) continue; - - try { - detector.onDetected(type); - } catch { - // Ignore - } - if (!detector.persistent) { - detector.justFired = true; - consumed = true; - } - } - - if (consumed) { - typeDetectors = typeDetectors.filter((d) => d.persistent || !d.justFired); - } -} - -declare module "./createElementIntercept" {} -interface TypeDetector { justFired?: boolean } - -/** Returns the collapse depth a parent should inherit from its children (max of any real child, 0 if none). */ -function inspectCollapseChild(child: any): number { - if (child == null || child === false || typeof child !== "object") return 0; - return collapseMarks.get(child) ?? 0; -} - -function inheritedCollapseDepth(props: any, rest: any[] = []): number { - let deepest = 0; - - const children = props?.children; - if (children != null) { - if (Array.isArray(children)) { - for (const child of children) deepest = Math.max(deepest, inspectCollapseChild(child)); - } else { - deepest = Math.max(deepest, inspectCollapseChild(children)); - } - } - - for (const child of rest) deepest = Math.max(deepest, inspectCollapseChild(child)); - return deepest; -} - -/** Returns a replacement type if this element should be intercepted, `null` to render nothing, or `undefined` to pass through unchanged. */ -function resolveReplacement(type: any, props: any, rest: any[]): { type: any; props: any; collapse?: number } | null | undefined { - runTypeDetectors(type); - - let effectiveProps = props; - if (effectiveProps) { - for (const { predicate, transform } of propsTransforms) { - try { - if (predicate(effectiveProps, type, rest)) { - effectiveProps = transform(effectiveProps); - } - } catch { - // Ignore - } - } - } - - if (effectiveProps) { - for (const { predicate, replacement } of propsIntercepts) { - try { - if (predicate(effectiveProps, type, rest)) { - return replacement ? { type: replacement, props: effectiveProps } : null; - } - } catch { - // Ignore - } - } - } - - const entry = intercepts.get(type); - if (entry) { - return { - type: entry.replacement, - props: entry.extraProps ? { ...effectiveProps, ...entry.extraProps } : effectiveProps, - collapse: entry.collapseAncestors, - }; - } - - // A wrapper whose only child was already collapsed - zero it out too and pass the remaining budget up. - const inherited = inheritedCollapseDepth(effectiveProps, rest); - if (inherited > 0) { - return { - type, - props: { ...effectiveProps, style: [effectiveProps?.style, COLLAPSE_STYLE] }, - collapse: inherited - 1, - }; - } - - if (effectiveProps !== props) { - return { type, props: effectiveProps }; - } - - return undefined; -} - -// after() only sees the return value, not the call itself, so we mutate the element in place -// rather than substituting args beforehand. -function applyResolved(res: any, type: any, props: any, rest: any[]) { - if (!res || typeof res !== "object") return res; - const resolved = resolveReplacement(type, props, rest); - if (resolved === null) { - res.type = () => null; - return res; - } - if (resolved) { - res.type = resolved.type; - res.props = resolved.props ?? null; - if (resolved.collapse && resolved.collapse > 0) { - collapseMarks.set(res, resolved.collapse); - } - } - return res; -} - -export function patchCreateElement(cleanups: (() => void)[]) { - if (isPatched) return; - isPatched = true; - - if (React.createElement) { - cleanups.push( - after("createElement", React, (args: any[], res: any) => applyResolved(res, args[0], args[1], args.slice(2))) - ); - } else { - console.warn("[ServerDrawer] React.createElement is null, skipping patch"); - } - - // Discord compiles JSX through jsx/jsxs/jsxDEV, not React.createElement - scan for those - // runtimes by shape (names are mangled) and keep scanning since Metro registers lazily. - const patchedJsxRuntimes = new WeakSet(); - - function isJsxRuntime(m: any): boolean { - return typeof m?.jsx === "function" || typeof m?.jsxs === "function" || typeof m?.jsxDEV === "function"; - } - - function patchJsxObject(runtime: any) { - if (patchedJsxRuntimes.has(runtime)) return; - patchedJsxRuntimes.add(runtime); - for (const key of ["jsx", "jsxs", "jsxDEV"] as const) { - if (typeof runtime[key] !== "function") continue; - cleanups.push( - after(key, runtime, (args: any[], res: any) => applyResolved(res, args[0], args[1], args.slice(2))) - ); - } - } - - function patchJsxModule(def: any) { - if (!def?.publicModule?.exports) return; - const exports = def.publicModule.exports; - - try { - if (isJsxRuntime(exports)) patchJsxObject(exports); - - const dflt = exports.default; - if (dflt != null && isJsxRuntime(dflt)) patchJsxObject(dflt); - } catch { - // Ignore - } - } - - function scanAndPatchJsxRuntimes() { - const modules = (window as any)?.modules; - if (!modules) return; - for (const id in modules) { - const def = modules[id]; - if (!def?.isInitialized) continue; - patchJsxModule(def); - } - } - - scanAndPatchJsxRuntimes(); - - // Fast while the app boots, then slow, then stop. - let ticks = 0; - let timer: ReturnType | undefined = setInterval(() => { - scanAndPatchJsxRuntimes(); - if (++ticks === 50 && timer) { // ~5s at 100ms - clearInterval(timer); - timer = setInterval(() => { - scanAndPatchJsxRuntimes(); - if (++ticks >= 75 && timer) { // + ~25s at 1s - clearInterval(timer); - timer = undefined; - } - }, 1000); - } - }, 100); - - cleanups.push(() => { - if (timer) clearInterval(timer); - timer = undefined; - isPatched = false; - intercepts.clear(); - propsIntercepts.length = 0; - propsTransforms.length = 0; - typeDetectors = []; - detectorKeys.clear(); - }); -} diff --git a/plugins/server-drawer/src/patches/getQuestAsset.ts b/plugins/server-drawer/src/patches/getQuestAsset.ts deleted file mode 100644 index 71b5ac1..0000000 --- a/plugins/server-drawer/src/patches/getQuestAsset.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { findByProps } from "@vendetta/metro"; - -const TAG = "[ServerDrawer]"; - -export function patchGetQuestAsset(cleanups: (() => void)[]): boolean { - const mod = findByProps("getQuestAsset"); - if (!mod?.getQuestAsset) { - console.log(TAG, "WARN: getQuestAsset not found"); - return false; - } - const orig = mod.getQuestAsset; - mod.getQuestAsset = function (...args: any[]) { - try { - return orig.apply(this, args); - } catch { - return { url: null, isAnimated: false }; - } - }; - cleanups.push(() => { mod.getQuestAsset = orig; }); - return true; -} diff --git a/plugins/server-drawer/src/patches/hideGuildsBar.tsx b/plugins/server-drawer/src/patches/hideGuildsBar.tsx deleted file mode 100644 index 17e57af..0000000 --- a/plugins/server-drawer/src/patches/hideGuildsBar.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; -import { View } from "react-native"; -import { registerPropsTransform, registerTypeDetector, registerIntercept } from "./createElementIntercept"; - -const TAG = "[ServerDrawer]"; -const SD_NOTHING_TEST_ID = "ServerDrawerNothing"; - -function Nothing() { - return React.createElement(View, { - style: { - display: "none", - width: 0, - minWidth: 0, - maxWidth: 0, - flexGrow: 0, - flexShrink: 0, - flexBasis: 0, - margin: 0, - padding: 0, - borderWidth: 0, - overflow: "hidden", - }, - }); -} - -// The parent creates GuildsBar via its outer React.memo wrapper object, which has no own -// .name/.displayName at all - those only exist on the memo's inner function, at -// type.type.name/type.type.displayName. -function isGuildsBar(type: any): boolean { - return type?.name === "GuildsBar" || type?.displayName === "GuildsBar" || - type?.type?.name === "GuildsBar" || type?.type?.displayName === "GuildsBar"; -} - -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; -} - -export function patchHideGuildsBar(cleanups: (() => void)[]): boolean { - // Rendering GuildsBar as nothing alone still leaves its space reserved - the immediate parent - // carries an explicit width in its own style, so display: none on the child doesn't reclaim - // it. This zeroes the parent wrapper too once it spots the hidden marker as a child. - registerPropsTransform( - (props: any, _type: any, rest: any[]) => - hasChildWithTestID(props?.children, rest, SD_NOTHING_TEST_ID), - (props: any) => ({ - ...props, - style: [ - props?.style, - { - display: "none", - width: 0, - minWidth: 0, - maxWidth: 0, - flexGrow: 0, - flexShrink: 0, - flexBasis: 0, - margin: 0, - padding: 0, - borderWidth: 0, - overflow: "hidden", - }, - ], - }), - ); - - registerTypeDetector("ServerDrawer.HideGuildsBar", isGuildsBar, (realGuildsBar) => { - registerIntercept(realGuildsBar, Nothing, { testID: SD_NOTHING_TEST_ID }, { collapseAncestors: 6 }); - console.log(TAG, "PATCH: found a real GuildsBar reference, now rendering nothing"); - }, { persistent: true }); - console.log(TAG, "PATCH: watching for the real GuildsBar to appear"); - return true; -} diff --git a/plugins/server-drawer/src/patches/mobileQuestDock.ts b/plugins/server-drawer/src/patches/mobileQuestDock.ts deleted file mode 100644 index 2fce38b..0000000 --- a/plugins/server-drawer/src/patches/mobileQuestDock.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { findByProps } from "@vendetta/metro"; - -const TAG = "[ServerDrawer]"; - -export function patchMobileQuestDock(cleanups: (() => void)[]): boolean { - const mod = findByProps("useMobileQuestDock"); - if (!mod.useMobileQuestDock) { - console.log(TAG, "WARN: useMobileQuestDock not found"); - return false; - } - const orig = mod.useMobileQuestDock; - mod.useMobileQuestDock = function (...args: any[]) { - const real = orig.apply(this, args); - if (real) return real; - return { - id: "server-drawer", - config: { - quest_content_type: 0, - assets: { questBarHeroVideo: null, questBarHero: null }, - features: [], - }, - userStatus: { enrolledAt: "2099-01-01", claimedAt: null }, - benefits: { rewards: [] }, - guildId: "0", - tasks: [], - }; - }; - cleanups.push(() => { mod.useMobileQuestDock = orig; }); - return true; -} diff --git a/plugins/server-drawer/src/patches/questDockBase.ts b/plugins/server-drawer/src/patches/questDockBase.ts deleted file mode 100644 index af936b5..0000000 --- a/plugins/server-drawer/src/patches/questDockBase.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { findByProps } from "@vendetta/metro"; - -const TAG = "[ServerDrawer]"; - -export function patchQuestDockBase(cleanups: (() => void)[]): boolean { - const mod = findByProps("useIsMobileQuestDockRenderedBase"); - if (!mod?.useIsMobileQuestDockRenderedBase) { - console.log(TAG, "WARN: useIsMobileQuestDockRenderedBase not found"); - return false; - } - const orig = mod.useIsMobileQuestDockRenderedBase; - mod.useIsMobileQuestDockRenderedBase = function (...args: any[]) { - orig.apply(this, args); - return true; - }; - cleanups.push(() => { mod.useIsMobileQuestDockRenderedBase = orig; }); - return true; -} diff --git a/plugins/server-drawer/src/patches/questDockRender.ts b/plugins/server-drawer/src/patches/questDockRender.ts deleted file mode 100644 index 9567866..0000000 --- a/plugins/server-drawer/src/patches/questDockRender.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { findByProps } from "@vendetta/metro"; - -const TAG = "[ServerDrawer]"; - -export function patchQuestDockRender(cleanups: (() => void)[]): boolean { - const mod = findByProps("useIsMobileQuestDockRendered"); - if (!mod?.useIsMobileQuestDockRendered) { - console.log(TAG, "WARN: useIsMobileQuestDockRendered not found"); - return false; - } - const orig = mod.useIsMobileQuestDockRendered; - mod.useIsMobileQuestDockRendered = function (...args: any[]) { - orig.apply(this, args); - return true; - }; - cleanups.push(() => { mod.useIsMobileQuestDockRendered = orig; }); - return true; -} diff --git a/plugins/server-drawer/src/patches/transparentBackground.ts b/plugins/server-drawer/src/patches/transparentBackground.ts deleted file mode 100644 index cfda6f9..0000000 --- a/plugins/server-drawer/src/patches/transparentBackground.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { registerPropsTransform, registerPropsIntercept } from "./createElementIntercept"; - -const TAG = "[ServerDrawer]"; - -// The quest dock's outer wrapper carries an opaque card background (borderRadius: 24) via -// useAnimatedStyle - no component reference to intercept, so match on the computed style instead. -// The color itself varies in format (rgba vs hex), so borderRadius + "has a background color" is -// the stable signal, not the color string. -function flatten(style: any): Record { - if (!style) return {}; - if (Array.isArray(style)) return Object.assign({}, ...style.map(flatten)); - return style; -} - -function isQuestDockCard(props: any): boolean { - const s = flatten(props?.style); - return s.borderRadius === 24 && typeof s.backgroundColor === "string"; -} - -// Removing the card color reveals a second layer underneath: the quest's promotional hero photo. -// Matched by URL shape, not component, since the same image component renders ordinary guild/ -// folder icons everywhere else too. -function getSourceUri(props: any): string | undefined { - const source = props?.source; - if (!source) return undefined; - if (Array.isArray(source)) return source[0]?.uri; - return source.uri; -} - -function isQuestHeroImage(props: any): boolean { - const uri = getSourceUri(props); - return typeof uri === "string" && uri.includes("/quests/"); -} - -export function patchTransparentBackground(cleanups: (() => void)[]): boolean { - registerPropsTransform( - (props: any) => isQuestDockCard(props), - (props: any) => ({ - ...props, - style: [props?.style, { backgroundColor: "transparent" }], - }), - ); - registerPropsIntercept(isQuestHeroImage, null); - console.log(TAG, "PATCH: watching for the quest dock card background and hero image"); - return true; -} diff --git a/plugins/server-drawer/src/ui/NoteBox.tsx b/plugins/server-drawer/src/ui/NoteBox.tsx deleted file mode 100644 index 50a5055..0000000 --- a/plugins/server-drawer/src/ui/NoteBox.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { React, ReactNative } from "@vendetta/metro/common"; -import { findByProps } from "@vendetta/metro"; - -const { View, Text } = ReactNative; - -const colors = findByProps("colors", "unsafe_rawColors")?.colors; - -/** Small bordered callout for explanatory text in a settings screen. */ -export default function NoteBox({ children, style }: { children: any; style?: any }) { - const textColor = colors?.TEXT_MUTED ?? colors?.TEXT_NORMAL ?? "#96989d"; - - return ( - - {children} - - ); -} diff --git a/plugins/server-drawer/src/ui/Settings.tsx b/plugins/server-drawer/src/ui/Settings.tsx deleted file mode 100644 index e21e0b4..0000000 --- a/plugins/server-drawer/src/ui/Settings.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { React } from "@vendetta/metro/common"; -import { storage } from "@vendetta/plugin"; -import { useProxy } from "@vendetta/storage"; -import { TableRowGroup, TableSwitchRow } from "@shared/ui/table"; -import SettingsScaffold from "@shared/ui/SettingsScaffold"; -import NoteBox from "./NoteBox"; - -export default function Settings() { - useProxy(storage); - - return ( - - - 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, and the + button creates or joins a server through Discord's own modal. - - - { storage.hideDmTile = v; }} - /> - { storage.showGuildNames = v; }} - /> - - - ); -} diff --git a/plugins/server-drawer/src/utils/theme.ts b/plugins/server-drawer/src/utils/theme.ts deleted file mode 100644 index 049dfb4..0000000 --- a/plugins/server-drawer/src/utils/theme.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { findByProps, findByStoreName } from "@vendetta/metro"; - -const Flux = findByProps("useStateFromStores"); -const colors = findByProps("colors", "unsafe_rawColors")?.colors; -const ExpandedGuildFolderStore = findByStoreName("ExpandedGuildFolderStore"); - -export function useTheme() { - return { - text: colors?.TEXT_NORMAL ?? "#dbdee1", - folder: "#5865f2", - hover: colors?.STATE_LAYER_PRESS ?? "rgba(255,255,255,0.06)", - }; -} - -export function useFolderExpanded(folderId: string | number): boolean { - return Flux?.useStateFromStores?.( - [ExpandedGuildFolderStore], - () => { - const folders = ExpandedGuildFolderStore?.getExpandedFolders?.(); - return folders instanceof Set ? folders.has(folderId) : false; - }, - [folderId], - ) ?? false; -} - -export interface GuildNode { - type: string; - id: string | number; - name?: string; - color?: number | null; - expanded?: boolean; - children: GuildNode[]; -}