Fix RoseUtils settings crash, ServerDrawer navigation/icons, DM tile, mark-all-read
This commit is contained in:
parent
ffc4e301f8
commit
137ccb7e43
12 changed files with 273 additions and 94 deletions
|
|
@ -26,7 +26,7 @@ To install: open Discord with Revenge installed, go to **Settings → Plugins**,
|
|||
| FakeProfileThemesAndEffects | Revived | Profile theming and profile effects without Nitro. Fork of [shipwr3ckd's FPTE](https://github.com/shipwr3ckd/FPTE) - fixed a crash on every profile-editor open, confirmed via on-device diagnostics. | `https://rp.jarviscli.dev/fake-profile-themes-and-effects/install/` |
|
||||
| BetterTimestamps | Revived | Calendar-relative, fully relative, ISO 8601, or a custom moment.js timestamp format, for both messages and day dividers. | `https://rp.jarviscli.dev/better-timestamps/install/` |
|
||||
| CopyRoleColor | Revived | Long-press a role pill to copy its color as hex. | `https://rp.jarviscli.dev/copy-role-color/install/` |
|
||||
| ServerDrawer | Revived | Replaces the Quest Dock with an expandable server grid, hides the left server rail down to a single DMs icon, long-press a server for its context menu, one-tap create/join, auto-collapse folders. Fork of [kmmiio99o's ServerDrawer](https://github.com/kmmiio99o/vd-plugins/tree/main/plugins/ServerDrawer) - fixed an unguarded lookup that could silently skip every patch after it. Folder options adapted from [fres621's BetterFolders](https://github.com/fres621/vendetta-plugins/tree/master/plugins/BetterFolders), rebuilt against the drawer's own folders since BetterFolders' target component no longer exists. | `https://rp.jarviscli.dev/server-drawer/install/` |
|
||||
| ServerDrawer | Revived | Replaces the Quest Dock with an expandable server grid - DMs, servers, folders, one-tap create/join, and mark-all-as-read, all in one drawer, with the left server rail hidden. Fork of [kmmiio99o's ServerDrawer](https://github.com/kmmiio99o/vd-plugins/tree/main/plugins/ServerDrawer) - fixed an unguarded lookup that could silently skip every patch after it, guild icons that never loaded, and drawer taps that didn't navigate. Folder options adapted from [fres621's BetterFolders](https://github.com/fres621/vendetta-plugins/tree/master/plugins/BetterFolders), rebuilt against the drawer's own folders since BetterFolders' target component no longer exists. | `https://rp.jarviscli.dev/server-drawer/install/` |
|
||||
| YouBar+ | Revived | Up to 2 configurable buttons (None/Direct Messages/Settings) on the YouBar, in your chosen order. Fork of [Purple-EyeZ's YouBar+](https://github.com/Purple-EyeZ/RevengePlugins/tree/main/plugins/YouBarPlus) - fixed a startup race condition where the button patch could silently never apply for an entire session. | `https://rp.jarviscli.dev/you-bar-plus/install/` |
|
||||
|
||||
### Productivity
|
||||
|
|
@ -44,7 +44,7 @@ To install: open Discord with Revenge installed, go to **Settings → Plugins**,
|
|||
| --- | --- | --- | --- |
|
||||
| ViewRaw | Revived | See a message's raw JSON via long-press - searchable, syntax-highlighted, and copyable. Fork of [bwlok's ViewRaw](https://github.com/bwlok/revenge-plugins/tree/master/plugins/ViewRaw), GPLv3. | `https://rp.jarviscli.dev/view-raw/install/` |
|
||||
| Key Inspector | New | One-tap scan of every internal key this repo's plugins depend on, plus a full dump of Revenge's plugin API tree - for diagnosing crashes entirely from a phone. | `https://rp.jarviscli.dev/key-inspector/install/` |
|
||||
| RoseUtils | Revived | Spotify listen-along, a Tenor GIF fix, colorful channels, and more - independently toggleable modules. Fork of [nexpid's NexxUtils](https://github.com/nexpid/RevengePlugins/tree/main/src/plugins/nexxutils), CC-BY 4.0. | `https://rp.jarviscli.dev/rose-utils/install/` |
|
||||
| RoseUtils | Revived | Spotify listen-along, a Tenor GIF fix, colorful channels, enforced Developer Mode, and more - independently toggleable modules. Fork of [nexpid's NexxUtils](https://github.com/nexpid/RevengePlugins/tree/main/src/plugins/nexxutils), CC-BY 4.0 - fixed a build bug that crashed the settings screen and silently dropped every module's patches. | `https://rp.jarviscli.dev/rose-utils/install/` |
|
||||
|
||||
"Revived" means the plugin previously existed elsewhere, stopped working after a Discord/API update, and has been rebuilt here.
|
||||
"Rebuilt" means it's a from-scratch reimplementation of a previously-broken plugin with expanded features. "New" means
|
||||
|
|
|
|||
|
|
@ -32,7 +32,11 @@ export interface ModuleMeta {
|
|||
export type AnyModule = Module<Record<string, ModuleSetting>>;
|
||||
|
||||
class Patches {
|
||||
private store: (() => void)[] = [];
|
||||
private store: (() => void)[];
|
||||
|
||||
constructor() {
|
||||
this.store = [];
|
||||
}
|
||||
|
||||
add(patch: () => void) {
|
||||
this.store.push(patch);
|
||||
|
|
@ -55,16 +59,24 @@ export class Module<Settings extends Record<string, ModuleSetting>> {
|
|||
label: string;
|
||||
meta: ModuleMeta;
|
||||
settings: Settings;
|
||||
errors: Record<string, string> = {};
|
||||
patches = new Patches();
|
||||
errors: Record<string, string>;
|
||||
patches: Patches;
|
||||
|
||||
private handlers: {
|
||||
onStart: (this: Module<Settings>) => void;
|
||||
onStop: (this: Module<Settings>) => void;
|
||||
};
|
||||
private started = false;
|
||||
private listeners = new Set<() => void>();
|
||||
private started: boolean;
|
||||
private listeners: Set<() => void>;
|
||||
|
||||
// Assigned in the constructor body rather than as class-field initializers - this repo's
|
||||
// SWC build config lowers `class` to an ES5-style function for compatibility, and that pass
|
||||
// doesn't also handle the separate (newer) class-fields proposal, so field initializers like
|
||||
// `errors: Record<string, string> = {};` were silently getting dropped from the compiled
|
||||
// output entirely. Confirmed by reading the built bundle directly: the compiled constructor
|
||||
// only set id/label/meta/settings/handlers, nothing else - every Module instance was missing
|
||||
// errors/patches/started/listeners, which crashed the settings screen on `Object.entries
|
||||
// (module.errors)` and made every module's own patches silently no-op.
|
||||
constructor({ id, label, meta, settings, handlers }: {
|
||||
id: string;
|
||||
label: string;
|
||||
|
|
@ -80,6 +92,10 @@ export class Module<Settings extends Record<string, ModuleSetting>> {
|
|||
this.meta = meta;
|
||||
this.settings = settings ?? ({} as Settings);
|
||||
this.handlers = handlers;
|
||||
this.errors = {};
|
||||
this.patches = new Patches();
|
||||
this.started = false;
|
||||
this.listeners = new Set();
|
||||
}
|
||||
|
||||
get storage(): { enabled: boolean; options: { [k in keyof Settings]: any } } {
|
||||
|
|
|
|||
39
plugins/rose-utils/src/modules/EnforceDeveloperMode/index.ts
Normal file
39
plugins/rose-utils/src/modules/EnforceDeveloperMode/index.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { findByProps } from "@vendetta/metro";
|
||||
import { fluxSubscribe } from "@shared/lib/flux";
|
||||
import { Module, ModuleCategory } from "../../lib/Module";
|
||||
|
||||
// Confirmed against decompiled current-build Discord source: Developer Mode is a proto-backed
|
||||
// setting exposed as DeveloperMode.getSetting()/updateSetting(bool), part of the same
|
||||
// modules/user_settings/UserSettings.tsx module that exports every other appearance/advanced
|
||||
// setting (DarkSidebar, MessageDisplayCompact, etc - all built the same way via defineProtoSetting).
|
||||
// It resets itself more than it should on Revenge - rather than guess at why, this just re-asserts
|
||||
// it every time the user's settings proto changes, which is the same event Discord's own settings
|
||||
// UI reacts to.
|
||||
const UserSettingsModule = findByProps("DeveloperMode", "DarkSidebar");
|
||||
|
||||
export default new Module({
|
||||
id: "enforce-developer-mode",
|
||||
label: "Enforce Developer Mode",
|
||||
meta: {
|
||||
sublabel: "Keeps Developer Mode switched on - it tends to reset itself on Revenge-based clients",
|
||||
category: ModuleCategory.Fixes,
|
||||
},
|
||||
handlers: {
|
||||
onStart() {
|
||||
const DeveloperMode = UserSettingsModule?.DeveloperMode;
|
||||
if (!DeveloperMode?.getSetting || !DeveloperMode?.updateSetting) return;
|
||||
|
||||
const enforce = () => {
|
||||
try {
|
||||
if (!DeveloperMode.getSetting()) DeveloperMode.updateSetting(true);
|
||||
} catch {
|
||||
// Best-effort - if this particular check throws, the next settings change retries it.
|
||||
}
|
||||
};
|
||||
|
||||
enforce();
|
||||
this.patches.add(fluxSubscribe("USER_SETTINGS_PROTO_UPDATE", enforce));
|
||||
},
|
||||
onStop() {},
|
||||
},
|
||||
});
|
||||
|
|
@ -3,8 +3,9 @@ import TenorGifFix from "./TenorGifFix";
|
|||
import SendSpotifyInvite from "./SendSpotifyInvite";
|
||||
import Minimod from "./Minimod";
|
||||
import ColorfulChannels from "./ColorfulChannels";
|
||||
import EnforceDeveloperMode from "./EnforceDeveloperMode";
|
||||
|
||||
// BetterComponents (Material-style Switch/Toast/Alert redesign) isn't ported - it needs
|
||||
// react-native-reanimated, which isn't part of this build, and it was already disabled by
|
||||
// default upstream due to known issues ("the great component functionification of 2025").
|
||||
export default [SpotifyListenAlong, TenorGifFix, SendSpotifyInvite, Minimod, ColorfulChannels];
|
||||
export default [SpotifyListenAlong, TenorGifFix, SendSpotifyInvite, Minimod, ColorfulChannels, EnforceDeveloperMode];
|
||||
|
|
|
|||
45
plugins/server-drawer/src/components/DmTile.tsx
Normal file
45
plugins/server-drawer/src/components/DmTile.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import React from "react";
|
||||
import { View, Pressable, Image, StyleSheet } from "react-native";
|
||||
import { findByProps, findByName } from "@vendetta/metro";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
|
||||
const ICON = 48;
|
||||
|
||||
const ChatIcon = getAssetIDByName("ChatIcon");
|
||||
const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes");
|
||||
const ChannelActions = findByProps("selectPrivateChannel");
|
||||
const SelectedChannelStore = findByName("SelectedChannelStore");
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const GuildStore = findByProps("getGuildId");
|
||||
const colors = findByProps("colors", "unsafe_rawColors")?.colors;
|
||||
|
||||
function openDms() {
|
||||
Haptic?.triggerHapticFeedback(Haptic.HapticFeedbackTypes.SOFT);
|
||||
if (ChannelActions?.selectPrivateChannel) {
|
||||
const lastChannelId = SelectedChannelStore?.getLastSelectedChannelId?.();
|
||||
ChannelActions.selectPrivateChannel(lastChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
// Was previously a full replacement for the native left server rail (rendered instead of it) -
|
||||
// moved into the drawer's own grid instead, as the first tile, so DMs live in the same place as
|
||||
// every other server rather than in a leftover spot from the sidebar this plugin otherwise hides.
|
||||
export default function DmTile() {
|
||||
const selected = Flux?.useStateFromStores?.(
|
||||
[GuildStore],
|
||||
() => GuildStore?.getGuildId?.() == null,
|
||||
) ?? false;
|
||||
|
||||
return (
|
||||
<Pressable onPress={openDms} style={st.outer}>
|
||||
<View style={[st.icon, { backgroundColor: selected ? (colors?.BG_ACCENT ?? "#5865f2") : "rgba(128,128,128,0.24)" }]}>
|
||||
<Image source={ChatIcon} style={{ width: 24, height: 24, tintColor: "#fff" }} />
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const st = StyleSheet.create({
|
||||
outer: { width: ICON, height: ICON },
|
||||
icon: { width: ICON, height: ICON, borderRadius: 16, alignItems: "center", justifyContent: "center" },
|
||||
});
|
||||
|
|
@ -3,10 +3,14 @@ import { View, Text, Image, StyleSheet } from "react-native";
|
|||
import { findByStoreName, findByProps } from "@vendetta/metro";
|
||||
|
||||
const GuildStore = findByStoreName("GuildStore");
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const colors = findByProps("colors", "unsafe_rawColors")?.colors;
|
||||
|
||||
export default function GuildIcon({ id, size = 48 }: { id: string; size?: number }) {
|
||||
const g = GuildStore?.getGuild(id);
|
||||
// Guild data streams in progressively after connect, so a one-shot GuildStore.getGuild(id)
|
||||
// taken at mount can miss it entirely and never recover - subscribing means the icon fills in
|
||||
// as soon as its guild finishes loading instead of staying blank for the rest of the session.
|
||||
const g = Flux?.useStateFromStores?.([GuildStore], () => GuildStore?.getGuild?.(id), [id]) ?? null;
|
||||
if (!g) return null;
|
||||
const rad = size >= 40 ? 16 : 8;
|
||||
if (g.icon) {
|
||||
|
|
|
|||
76
plugins/server-drawer/src/components/MarkAllReadButton.tsx
Normal file
76
plugins/server-drawer/src/components/MarkAllReadButton.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import React from "react";
|
||||
import { Pressable, Image, StyleSheet } from "react-native";
|
||||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { showToast } from "@vendetta/ui/toasts";
|
||||
import { lazy } from "@shared/lib/lazy";
|
||||
|
||||
const ICON = 48;
|
||||
|
||||
const CheckIcon = getAssetIDByName("CheckmarkLargeIcon");
|
||||
const GuildStore = findByStoreName("GuildStore");
|
||||
const GuildChannelStore = findByStoreName("GuildChannelStore");
|
||||
|
||||
// Lazy because bulkAck lives on an action-creator module that, like routing, may not be required
|
||||
// by Discord's own code yet at the moment this plugin's bundle loads.
|
||||
const getHaptic = lazy(() => findByProps("triggerHapticFeedback", "HapticFeedbackTypes"));
|
||||
const getBulkAck = lazy(() => findByProps("bulkAck", "ackChannel"));
|
||||
const getReadStateTypes = lazy(() => findByProps("ReadStateTypes", "UnreadSetting")?.ReadStateTypes);
|
||||
|
||||
// Mirrors the payload shape Discord's own bulk-ack call sites use (confirmed against decompiled
|
||||
// current-build source, e.g. modules/guild/markGuildsAsRead.tsx): an array of
|
||||
// { channelId, readStateType, messageId } acked in one dispatch. Built here from GuildStore +
|
||||
// GuildChannelStore directly rather than calling markGuildsAsRead itself, since that module has no
|
||||
// named export to reliably target with findByProps (it's a bare default export).
|
||||
function markAllServersRead() {
|
||||
const bulkAck = getBulkAck();
|
||||
const ReadStateTypes = getReadStateTypes();
|
||||
if (!bulkAck?.bulkAck || ReadStateTypes?.CHANNEL == null || !GuildStore?.getGuilds || !GuildChannelStore?.getChannels) {
|
||||
showToast("Couldn't find Discord's read-state action - this may be unavailable on your version.", undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const channels: { channelId: string; readStateType: number; messageId: string | null }[] = [];
|
||||
|
||||
for (const guildId of Object.keys(GuildStore.getGuilds())) {
|
||||
const byType = GuildChannelStore.getChannels(guildId) ?? {};
|
||||
for (const entries of Object.values(byType)) {
|
||||
if (!Array.isArray(entries)) continue;
|
||||
for (const entry of entries as any[]) {
|
||||
const channel = entry?.channel;
|
||||
if (!channel?.id) continue;
|
||||
channels.push({
|
||||
channelId: channel.id,
|
||||
readStateType: ReadStateTypes.CHANNEL,
|
||||
messageId: channel.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" },
|
||||
});
|
||||
|
|
@ -1,22 +1,27 @@
|
|||
import React from "react";
|
||||
import { View, Text, Pressable, Animated, Dimensions, StyleSheet, BackHandler } from "react-native";
|
||||
import { find, findByProps, findByStoreName } from "@vendetta/metro";
|
||||
import { lazy } from "@shared/lib/lazy";
|
||||
import { GuildNode } from "../utils/theme";
|
||||
import GuildItem from "./GuildItem";
|
||||
import FolderItem from "./FolderItem";
|
||||
import DmTile from "./DmTile";
|
||||
import MarkAllReadButton from "./MarkAllReadButton";
|
||||
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const SortedGuildStore = findByStoreName("SortedGuildStore");
|
||||
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;
|
||||
// These are looked up lazily (retried on first real use, not cached the moment this file is
|
||||
// required) because this whole module gets evaluated as soon as the plugin's bundle loads - which
|
||||
// can be before Discord's own routing/navigation modules have been required yet. A one-shot
|
||||
// findByProps() here would then permanently cache undefined even though the real module shows up
|
||||
// moments later, which is exactly what made tapping a server in the drawer silently do nothing.
|
||||
const getRootNav = lazy(() => findByProps("getRootNavigationRef"));
|
||||
const getHaptic = lazy(() => findByProps("triggerHapticFeedback", "HapticFeedbackTypes"));
|
||||
const getRouting = lazy(() => findByProps("transitionToGuild"));
|
||||
const getCreateJoinGuildMod = lazy(() => find((m: any) => typeof m?.handleCreateJoinGuildPress === "function"));
|
||||
const getCirclePlusIcon = lazy(() => find((m: any) => m?.CirclePlusIcon)?.CirclePlusIcon);
|
||||
const getRawColors = lazy(() => findByProps("colors", "unsafe_rawColors")?.unsafe_rawColors);
|
||||
|
||||
const ExternalCoordinationMod = find((m: any) => m?.QuestDockExternalCoordinationContext);
|
||||
const ExternalContext = ExternalCoordinationMod?.QuestDockExternalCoordinationContext;
|
||||
|
|
@ -36,17 +41,21 @@ function CreateJoinButton() {
|
|||
}, [scale]);
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.SOFT);
|
||||
CreateJoinGuildMod?.handleCreateJoinGuildPress?.();
|
||||
const haptic = getHaptic();
|
||||
haptic?.triggerHapticFeedback?.(haptic.HapticFeedbackTypes.SOFT);
|
||||
getCreateJoinGuildMod()?.handleCreateJoinGuildPress?.();
|
||||
}, []);
|
||||
|
||||
const CirclePlusIcon = getCirclePlusIcon();
|
||||
const rawColors = getRawColors();
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress} onPressIn={scaleDown} onPressOut={scaleUp}>
|
||||
<Animated.View style={[st.createJoin, { transform: [{ scale }] }]}>
|
||||
<Animated.View style={[st.createJoin, { backgroundColor: rawColors?.GREEN_360, transform: [{ scale }] }]}>
|
||||
{CirclePlusIcon ? (
|
||||
<CirclePlusIcon size="md" color={createJoinIconColor} />
|
||||
<CirclePlusIcon size="md" color={rawColors?.WHITE} />
|
||||
) : (
|
||||
<Text style={st.createJoinFallback}>{"+"}</Text>
|
||||
<Text style={[st.createJoinFallback, { color: rawColors?.WHITE }]}>{"+"}</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
|
|
@ -55,11 +64,20 @@ function CreateJoinButton() {
|
|||
|
||||
export default function ServerDrawerSheet({ gestureContext }: { gestureContext: any }) {
|
||||
const pick = React.useCallback((id: string) => {
|
||||
Haptic?.triggerHapticFeedback(Haptic.HapticFeedbackTypes.SOFT);
|
||||
if (Routing?.transitionToGuild) {
|
||||
Routing.transitionToGuild(id, null);
|
||||
const haptic = getHaptic();
|
||||
haptic?.triggerHapticFeedback?.(haptic.HapticFeedbackTypes.SOFT);
|
||||
|
||||
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 {
|
||||
RootNav?.getRootNavigationRef()?.navigate("guilds", { guildId: id });
|
||||
getRootNav()?.getRootNavigationRef?.()?.navigate("guilds", { guildId: id });
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
@ -110,12 +128,14 @@ export default function ServerDrawerSheet({ gestureContext }: { gestureContext:
|
|||
style={[st.grid, { paddingHorizontal: padX, gap: GAP }]}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<DmTile />
|
||||
{nodes.map((node) =>
|
||||
node.type === "folder"
|
||||
? <FolderItem key={node.id} node={node} onPick={pick} />
|
||||
: <GuildItem key={node.id} node={node} onPick={pick} />
|
||||
)}
|
||||
<CreateJoinButton />
|
||||
<MarkAllReadButton />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -137,12 +157,10 @@ const st = StyleSheet.create({
|
|||
width: ICON,
|
||||
height: ICON,
|
||||
borderRadius: 16,
|
||||
backgroundColor: createJoinBg,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
createJoinFallback: {
|
||||
color: createJoinIconColor,
|
||||
fontSize: 28,
|
||||
fontWeight: "700",
|
||||
lineHeight: 30,
|
||||
|
|
|
|||
|
|
@ -1,61 +1,13 @@
|
|||
import React from "react";
|
||||
import { Pressable, Image, View } from "react-native";
|
||||
import { find, findByName, findByProps } from "@vendetta/metro";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { find, findByName } from "@vendetta/metro";
|
||||
import { registerIntercept } from "./createElementIntercept";
|
||||
|
||||
const TAG = "[ServerDrawer]";
|
||||
|
||||
const ChatIcon = getAssetIDByName("ChatIcon");
|
||||
const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes");
|
||||
const ChannelActions = findByProps("selectPrivateChannel");
|
||||
const SelectedChannelStore = findByName("SelectedChannelStore");
|
||||
const FluxStores = findByProps("useStateFromStores");
|
||||
const useStateFromStores = FluxStores?.useStateFromStores;
|
||||
const GuildStore = findByProps("getGuildId");
|
||||
const colors = findByProps("colors", "unsafe_rawColors")?.colors;
|
||||
const Routes = findByProps("ME");
|
||||
const ME = Routes?.ME ?? "/channels/@me";
|
||||
|
||||
function openDms() {
|
||||
Haptic?.triggerHapticFeedback(Haptic.HapticFeedbackTypes.SOFT);
|
||||
if (ChannelActions?.selectPrivateChannel) {
|
||||
const lastChannelId = SelectedChannelStore?.getLastSelectedChannelId?.();
|
||||
ChannelActions.selectPrivateChannel(lastChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
function useIsInDms(): boolean {
|
||||
try {
|
||||
return GuildStore ? useStateFromStores([GuildStore], () => {
|
||||
const guildId = GuildStore?.getGuildId?.();
|
||||
return guildId == null || guildId === ME;
|
||||
}) : false;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function DmsGuildIcon() {
|
||||
const selected = useIsInDms();
|
||||
|
||||
const iconColor = selected
|
||||
? (colors?.WHITE ?? "#fff")
|
||||
: (colors?.INTERACTIVE_NORMAL ?? "#80848e");
|
||||
|
||||
return (
|
||||
<Pressable onPress={openDms} style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
|
||||
{selected ? (
|
||||
<View style={{
|
||||
position: "absolute", left: 0, top: "25%",
|
||||
width: 4, height: "50%", borderRadius: 2,
|
||||
backgroundColor: colors?.WHITE ?? "#fff",
|
||||
}} />
|
||||
) : null}
|
||||
<Image
|
||||
source={ChatIcon}
|
||||
style={{ width: 28, height: 28, tintColor: iconColor }}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
// Used to replace the sidebar with a single DMs icon in its place; DMs now live in the drawer's
|
||||
// own grid (as the first tile) instead, so the rail just goes away entirely.
|
||||
function EmptyGuildsBar() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findGuildsBar(): any {
|
||||
|
|
@ -83,10 +35,10 @@ export function patchHideGuildsBar(cleanups: (() => void)[]): boolean {
|
|||
}
|
||||
const orig = mod.default;
|
||||
|
||||
registerIntercept(orig, DmsGuildIcon);
|
||||
registerIntercept(orig, EmptyGuildsBar);
|
||||
|
||||
mod.default = function DmsGuildsBar() {
|
||||
return React.createElement(DmsGuildIcon);
|
||||
mod.default = function HiddenGuildsBar() {
|
||||
return React.createElement(EmptyGuildsBar);
|
||||
};
|
||||
mod.default.displayName = "GuildsBar";
|
||||
cleanups.push(() => { mod.default = orig; });
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ 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, and use the + button to create or join a server through Discord's own modal.
|
||||
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.
|
||||
</NoteBox>
|
||||
<FormSection title="Layout">
|
||||
<FormSwitchRow
|
||||
label="Hide the left server rail"
|
||||
subLabel="Replaces it with a single DMs icon since your servers now live in the drawer"
|
||||
subLabel="Your servers and DMs already live in the drawer, so the rail is redundant"
|
||||
value={storage.hideGuildsBar !== false}
|
||||
onValueChange={(v: boolean) => {
|
||||
storage.hideGuildsBar = v;
|
||||
|
|
|
|||
24
shared/lib/lazy.ts
Normal file
24
shared/lib/lazy.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Wraps a metro lookup so it isn't run until first actually used, and retries on every call until
|
||||
* it succeeds once (after which the result is cached forever). A plugin's top-level module code
|
||||
* runs as soon as its bundle is required, which can be before Discord's own code has required an
|
||||
* internal module it depends on - a one-shot findByX() at module scope can permanently cache
|
||||
* undefined even though the real module becomes available moments later. Only meant for values
|
||||
* used inside event handlers or plain (non-hook) render logic - wrapping something used as a
|
||||
* React hook (e.g. passed to useContext) in this can violate the Rules of Hooks, since the
|
||||
* resolved-or-not status is now allowed to change between renders of the same component instance.
|
||||
*/
|
||||
export function lazy<T>(resolve: () => T | null | undefined): () => T | undefined {
|
||||
let cached: T | undefined;
|
||||
let resolved = false;
|
||||
|
||||
return () => {
|
||||
if (resolved) return cached;
|
||||
const value = resolve();
|
||||
if (value != null) {
|
||||
cached = value;
|
||||
resolved = true;
|
||||
}
|
||||
return value ?? undefined;
|
||||
};
|
||||
}
|
||||
|
|
@ -223,8 +223,8 @@
|
|||
"category": "Utility",
|
||||
"status": "revived",
|
||||
"accent": "#F0637E",
|
||||
"tagline": "A grab-bag of independent utilities - Spotify listen-along, a Tenor fix, colorful channels, and more.",
|
||||
"note": "Fork of nexpid's NexxUtils. Fixed the reported \"render is not a function in SpotifyPlayButton\" crash - Discord converted it from a class component to a function component, confirmed against decompiled current-build source, so patching .prototype.render no longer worked. Also fixed six other lookups across the plugin that had the same unguarded pattern that crashed FPTE's whole bundle at load time - any one of them failing would have taken the entire plugin down, not just one module.",
|
||||
"tagline": "Spotify listen-along, a Tenor fix, colorful channels, and more - now actually working.",
|
||||
"note": "Fork of nexpid's NexxUtils. Fixed the reported \"render is not a function in SpotifyPlayButton\" crash - Discord converted it from a class component to a function component, confirmed against decompiled current-build source, so patching .prototype.render no longer worked. Also fixed six other lookups across the plugin that had the same unguarded pattern that crashed FPTE's whole bundle at load time - any one of them failing would have taken the entire plugin down, not just one module. A more serious bug was found and fixed after real device testing: opening RoseUtils' own settings screen crashed outright with \"Cannot convert undefined value to object\". Root cause, confirmed by reading the compiled bundle directly: this repo's SWC build config lowers class syntax to plain functions for compatibility, but doesn't also handle the separate class-fields transform, so every field initializer on the internal Module class (errors, patches, started, listeners) was silently dropped from the compiled output - every module was missing all four. Fixed by assigning them in the constructor body instead, which every build target handles the same way. Also added a new module: Enforce Developer Mode, which keeps Discord's Developer Mode switched on since it tends to reset itself on Revenge.",
|
||||
"howItWorks": "Each utility is its own independently toggleable module: Spotify listen-along buttons on activities, a fix for Tenor GIFs downloading as videos, an invite-to-listen-along button in the chat attachment menu, a fun \"minimod\" that shows member timeouts in chat, and colorful/NSFW-tagged channel icons. Every module tracks its own errors and shows a one-tap \"copy report\" row if something goes wrong, so a problem in one module is easy to report without digging through logs. The original's Material-Design component reskin (Switch/Toast/Alert) wasn't ported - it depends on react-native-reanimated, which isn't part of this build, and it was already disabled by default upstream due to known issues.",
|
||||
"features": [
|
||||
"Spotify listen-along buttons on activities (fixed)",
|
||||
|
|
@ -232,7 +232,8 @@
|
|||
"Send-a-Spotify-invite button in the attachment menu",
|
||||
"Minimod: shows member timeouts in chat",
|
||||
"Colorful, NSFW-tagged channel icons",
|
||||
"Per-module error tracking with one-tap copy for bug reports"
|
||||
"Per-module error tracking with one-tap copy for bug reports",
|
||||
"Enforce Developer Mode - keeps it on since it resets itself on Revenge-based clients"
|
||||
],
|
||||
"commands": [],
|
||||
"limitations": "The Material-Design component reskin from the original isn't included (needs react-native-reanimated). Each module was fixed and defensively guarded, but only the specific reported crash was verified against live decompiled source - the rest are guarded, not individually confirmed working on-device."
|
||||
|
|
@ -241,8 +242,8 @@
|
|||
"category": "Moderation & Appearance",
|
||||
"status": "revived",
|
||||
"accent": "#F97316",
|
||||
"tagline": "Turns the Quest Dock into an expandable server grid, with smarter folders.",
|
||||
"note": "Fork of kmmiio99o's ServerDrawer. One lookup (useMobileQuestDock) was missing an optional-chaining guard - if that lookup came back empty, the whole onLoad threw and every patch after it in the sequence never got applied, not just that one surface. Fixed, plus every patch call is now wrapped so a single bad lookup only skips its own surface. Folder options (auto-collapse, hide preview icons) are adapted from fres621's BetterFolders - its icon-hiding half patched a \"GuildContainer\" component that no longer exists anywhere in current Discord builds (confirmed against decompiled source), and only applied to the native server rail anyway, which this plugin already replaces - so it's been rebuilt as a feature of the drawer's own folders instead.",
|
||||
"tagline": "An expandable server grid with DMs, smarter folders, and mark-all-read built in.",
|
||||
"note": "Fork of kmmiio99o's ServerDrawer. One lookup (useMobileQuestDock) was missing an optional-chaining guard - if that lookup came back empty, the whole onLoad threw and every patch after it in the sequence never got applied, not just that one surface. Fixed, plus every patch call is now wrapped so a single bad lookup only skips its own surface. Folder options (auto-collapse, hide preview icons) are adapted from fres621's BetterFolders - its icon-hiding half patched a \"GuildContainer\" component that no longer exists anywhere in current Discord builds (confirmed against decompiled source), and only applied to the native server rail anyway, which this plugin already replaces - so it's been rebuilt as a feature of the drawer's own folders instead. Two more bugs fixed after real device testing: guild icons that never loaded (GuildStore lookups weren't reactive to guild data streaming in after connect, so most servers just showed a bare unread dot with no icon) and taps in the drawer not navigating anywhere (several lookups - transitionToGuild, the create/join action, the root nav ref - were resolved once at plugin-load time, before Discord had necessarily required those modules yet, same class of bug as YouBar+'s startup race). Both fixed; DMs also moved from a leftover sidebar icon into the drawer itself, and a mark-all-servers-as-read button was added.",
|
||||
"howItWorks": "Hijacks the Quest Dock - the strip Discord normally uses for daily quests, right above the tab bar - and puts a grid of your servers there instead. Tap it to expand, tap a server to jump to it, long-press for the usual context menu (mark as read, notification settings, leave, etc). A + button opens Discord's own create/join-server modal. Optionally hides the left server rail down to a single DMs icon, since your servers now live in the drawer instead.",
|
||||
"features": [
|
||||
"Tap-to-expand server grid with folders, unread badges, and mention counts",
|
||||
|
|
@ -251,10 +252,12 @@
|
|||
"Optional: collapse the left server rail to a single DMs icon",
|
||||
"Toggle unread badges independently",
|
||||
"Auto-collapse other open folders when you open a new one",
|
||||
"Optional plain folder icon instead of the 4-server preview when collapsed"
|
||||
"Optional plain folder icon instead of the 4-server preview when collapsed",
|
||||
"DMs tile in the drawer instead of a separate sidebar icon",
|
||||
"Mark every channel in every server as read in one tap"
|
||||
],
|
||||
"commands": [],
|
||||
"limitations": "Depends on several internal Quest Dock lookups (11 in total) - each is individually guarded, so a future Discord rename degrades one surface at a time instead of breaking the whole plugin. Drawer position (left/right/top) is planned but not in this build yet - it currently only replaces the Quest Dock's native bottom position."
|
||||
"limitations": "Depends on several internal Quest Dock lookups (11 in total) - each is individually guarded, so a future Discord rename degrades one surface at a time instead of breaking the whole plugin. Drawer position (left/right/top) is still planned but not in this build yet - it currently only replaces the Quest Dock's native bottom position."
|
||||
},
|
||||
"you-bar-plus": {
|
||||
"category": "Moderation & Appearance",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue