RoseUtils: expand Developer Mode, add message/server dev-tools, drop Tenor fix

This commit is contained in:
Ally Founder 2026-08-02 11:46:04 -04:00
parent 97642009b3
commit 48662f828c
11 changed files with 660 additions and 113 deletions

View file

@ -0,0 +1,144 @@
import { before, after } from "@vendetta/patcher";
import { findByProps } from "@vendetta/metro";
import { React } from "@vendetta/metro/common";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { findInReactTree } from "@vendetta/utils";
// Injection technique ported from this repo's ViewRaw plugin (view-raw/src/index.tsx) - confirmed
// working there for adding a row to the message long-press action sheet. Generalized here so more
// than one RoseUtils module can each contribute their own rows through a single shared patch,
// instead of every module that wants this hook patching openLazy separately (which would stack
// duplicate patches, and duplicate "instance.__patched" bookkeeping, the moment more than one such
// module is enabled at once).
export interface MessageActionRow {
key: string;
label: string;
sublabel?: string;
icon?: string;
onPress: (message: any) => void;
}
export type MessageActionBuilder = (message: any) => MessageActionRow[];
const LazyActionSheet = findByProps("openLazy", "hideActionSheet");
const { ActionSheetRow } = findByProps("ActionSheetRow") ?? {};
const builders = new Set<MessageActionBuilder>();
let realUnpatch: (() => void) | null = null;
function buildRowElement(row: MessageActionRow, message: any) {
if (!ActionSheetRow) return null;
return React.createElement(ActionSheetRow, {
label: row.label,
subLabel: row.sublabel,
icon: row.icon ? React.createElement(ActionSheetRow.Icon, { source: getAssetIDByName(row.icon) }) : undefined,
onPress: () => {
try {
row.onPress(message);
} finally {
LazyActionSheet?.hideActionSheet?.();
}
},
key: row.key,
});
}
function collectRowElements(message: any) {
const elements: any[] = [];
for (const build of builders) {
try {
for (const row of build(message)) {
const el = buildRowElement(row, message);
if (el) elements.push(el);
}
} catch {
// A broken builder shouldn't block anyone else's rows from showing up.
}
}
return elements;
}
function patchOpenLazy(): () => void {
return before("openLazy", LazyActionSheet, ([component, key, msg]: any[]) => {
if (key !== "MessageLongPressActionSheet" || !msg?.message) return;
component.then((instance: any) => {
// Discord appears to reuse the same lazy-loaded instance across every action sheet
// open - only patch it once, and track the active message on the instance itself so
// re-opening on a different message doesn't need a fresh patch.
instance.__roseUtilsActiveMessage = msg.message;
if (instance.__roseUtilsPatched) return;
instance.__roseUtilsPatched = true;
after("default", instance, (_: any, result: any) => {
try {
const rows = collectRowElements(instance.__roseUtilsActiveMessage);
if (!rows.length) return;
// Real ActionSheetRow groups - search every group for one that already has
// ActionSheetRow-shaped children and insert there, falling back to a brand new
// group at the top if none match.
const groups = findInReactTree(
result,
(x: any) => Array.isArray(x) && x[0]?.type?.name === "ActionSheetRowGroup",
);
if (Array.isArray(groups) && groups.length && ActionSheetRow) {
let inserted = false;
for (const group of groups) {
const children = findInReactTree(
group,
(c: any) => Array.isArray(c) && c.some((child: any) => child?.type?.name === "ActionSheetRow"),
);
if (Array.isArray(children)) {
children.push(...rows);
inserted = true;
break;
}
}
if (!inserted && typeof groups.unshift === "function" && ActionSheetRow.Group) {
groups.unshift(React.createElement(ActionSheetRow.Group, {}, rows));
inserted = true;
}
if (inserted) return;
}
// Name-independent fallback: any array where every element already looks like
// an action sheet row (has both a label and an onPress).
const genericRowGroup = findInReactTree(
result,
(x: any) =>
Array.isArray(x) &&
x.length > 0 &&
x.every((el: any) => typeof el?.props?.label === "string" && typeof el?.props?.onPress === "function"),
);
if (Array.isArray(genericRowGroup)) genericRowGroup.push(...rows);
} catch {
// Best-effort - if the action sheet shape doesn't match any known strategy,
// the extra rows just don't appear rather than crashing the sheet.
}
});
});
});
}
/**
* Registers a row builder for the message long-press action sheet. Returns an unregister
* function. The underlying openLazy patch is reference-counted across every caller - it's only
* applied on the first registration and only removed once the last one unregisters, so modules
* can start/stop independently of each other's registration order.
*/
export function registerMessageAction(builder: MessageActionBuilder): () => void {
if (!realUnpatch && LazyActionSheet) realUnpatch = patchOpenLazy();
builders.add(builder);
return () => {
builders.delete(builder);
if (builders.size === 0 && realUnpatch) {
realUnpatch();
realUnpatch = null;
}
};
}

View file

@ -0,0 +1,15 @@
// Pure math, no Discord internals involved - Discord IDs are Twitter-style snowflakes: the top 42
// bits are milliseconds since the Discord epoch (2015-01-01T00:00:00.000Z).
const DISCORD_EPOCH = 1420070400000n;
export function decodeSnowflakeTimestamp(id: string): Date | null {
const trimmed = id.trim();
if (!/^\d{15,20}$/.test(trimmed)) return null;
try {
const ms = (BigInt(trimmed) >> 22n) + DISCORD_EPOCH;
return new Date(Number(ms));
} catch {
return null;
}
}

View file

@ -0,0 +1,152 @@
import { findByProps, findByStoreName } from "@vendetta/metro";
import { clipboard } from "@vendetta/metro/common";
import { showToast } from "@vendetta/ui/toasts";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { fluxSubscribe } from "@shared/lib/flux";
import { Module, ModuleCategory } from "../../lib/Module";
import { registerMessageAction, type MessageActionRow } from "../../lib/messageActionSheet";
// 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");
// Confirmed live via Key Inspector Eval (/root/eval-for-revenge/ru/activity-payload-capture.txt):
// caught a real LOCAL_ACTIVITY_UPDATE payload while Watch Together was running - the activity
// object is exactly {type, name, application_id, platform, party, secrets, flags, metadata}, snake
// case, matching the primary branch checked below. That capture was for the local user specifically
// (own activities dispatch through a local-first path, which is why PresenceStore.getActivities on
// your own ID came back empty even mid-session) - but it's the same public Gateway Activity object
// schema Discord broadcasts for everyone, which is what PresenceStore caches for *other* users -
// the actual case this code needs, since it looks up the message author, not the local user. Not
// yet caught a live PresenceStore.getActivities(otherUserId) call directly, but the shape match
// leaves nothing left to guess at.
const PresenceStore = findByStoreName("PresenceStore");
// Confirmed live via Key Inspector Eval (/root/eval-for-revenge/ru/all-checks.txt) against real
// slash-command messages: message.interaction is populated with {id, name, type, user,
// displayName} exactly as read below (message.type 20 = CHAT_INPUT_COMMAND on these).
// interactionMetadata/interaction_metadata are checked as a fallback for whichever build doesn't
// populate the legacy .interaction field.
function getCommandInteraction(message: any): { id: string; name?: string } | null {
const interaction = message?.interaction ?? message?.interactionMetadata ?? message?.interaction_metadata;
if (!interaction?.id) return null;
return { id: String(interaction.id), name: interaction.name };
}
function commandIdRows(message: any): MessageActionRow[] {
const interaction = getCommandInteraction(message);
if (!interaction) return [];
return [
{
key: "rose-utils-copy-command-id",
label: "Copy Command ID",
sublabel: interaction.name ? `/${interaction.name}` : "Slash command interaction",
icon: "ic_copy_24px",
onPress: () => {
clipboard.setString(interaction.id);
showToast("Copied command ID to clipboard", getAssetIDByName("ic_copy_24px"));
},
},
];
}
function activityIdRows(message: any): MessageActionRow[] {
if (!PresenceStore?.getActivities) return [];
const authorId = message?.author?.id;
if (!authorId) return [];
let activities: any[];
try {
activities = PresenceStore.getActivities(authorId);
} catch {
return [];
}
if (!Array.isArray(activities)) return [];
const withAppId = activities.filter((a) => a?.application_id ?? a?.applicationId);
if (!withAppId.length) return [];
return withAppId.map((activity, i) => {
const appId = String(activity.application_id ?? activity.applicationId);
return {
key: `rose-utils-copy-activity-id-${i}`,
label: withAppId.length > 1 ? `Copy Activity ID (${activity.name ?? i + 1})` : "Copy Activity/Game ID",
sublabel: activity.name,
icon: "ic_copy_24px",
onPress: () => {
clipboard.setString(appId);
showToast(`Copied ${activity.name ?? "activity"} ID to clipboard`, getAssetIDByName("ic_copy_24px"));
},
};
});
}
export default new Module({
id: "enforce-developer-mode",
label: "Developer Mode",
meta: {
sublabel: "Turns on Discord's real Developer Mode (all its native Copy ID options) and adds extra copy-ID tools slash commands and activities don't get natively",
category: ModuleCategory.Useful,
},
settings: {
keepNativeOn: {
label: "Keep native Developer Mode on",
subLabel: "Re-asserts Discord's own Developer Mode setting - it tends to reset itself on Revenge-based clients",
type: "toggle",
default: true,
},
copyCommandId: {
label: "Copy Command ID",
subLabel: "Adds a row to the message long-press menu for messages sent via a slash command",
type: "toggle",
default: true,
},
copyActivityId: {
label: "Copy Activity/Game ID",
subLabel: "Adds a row to the message long-press menu to copy the author's current game/activity application ID (not every activity type has one, e.g. a plain custom status)",
type: "toggle",
default: true,
},
},
handlers: {
onStart() {
if (this.storage.options.keepNativeOn) {
const DeveloperMode = UserSettingsModule?.DeveloperMode;
if (DeveloperMode?.getSetting && DeveloperMode?.updateSetting) {
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));
// Belt and suspenders: if whatever resets this doesn't go through
// USER_SETTINGS_PROTO_UPDATE for some reason, this still catches it within 5s
// instead of staying off for the rest of the session.
const interval = setInterval(enforce, 5000);
this.patches.add(() => clearInterval(interval));
}
}
if (this.storage.options.copyCommandId) {
this.patches.add(registerMessageAction(commandIdRows));
}
if (this.storage.options.copyActivityId) {
this.patches.add(registerMessageAction(activityIdRows));
}
},
onStop() {},
},
});

View file

@ -1,45 +0,0 @@
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));
// Belt and suspenders: if whatever resets this doesn't go through
// USER_SETTINGS_PROTO_UPDATE for some reason, this still catches it within 5s instead
// of staying off for the rest of the session.
const interval = setInterval(enforce, 5000);
this.patches.add(() => clearInterval(interval));
},
onStop() {},
},
});

View file

@ -0,0 +1,153 @@
import { clipboard } from "@vendetta/metro/common";
import { showToast } from "@vendetta/ui/toasts";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { Module, ModuleCategory } from "../../lib/Module";
import { registerMessageAction, type MessageActionRow } from "../../lib/messageActionSheet";
const COPY_ICON = "ic_copy_24px";
function copy(value: string, toastLabel: string) {
clipboard.setString(value);
showToast(toastLabel, getAssetIDByName(COPY_ICON));
}
// Every field read below (webhookId, nonce, attachments, messageReference, type/flags/timestamps)
// is part of Discord's own public message object schema - the same shape the client already
// deserializes the gateway/REST payload into before handing it to the action sheet - not
// internal-UI guesswork, unlike the activity-ID lookup in the Developer Mode module. webhookId,
// nonce, and messageReference are confirmed live via Key Inspector Eval
// (/root/eval-for-revenge/ru/all-checks.txt) against real interaction-response and reply messages.
function webhookIdRow(message: any): MessageActionRow[] {
const webhookId = message?.webhookId ?? message?.webhook_id;
if (!webhookId) return [];
return [
{
key: "rose-utils-copy-webhook-id",
label: "Copy Webhook ID",
icon: COPY_ICON,
onPress: () => copy(String(webhookId), "Copied webhook ID to clipboard"),
},
];
}
function nonceRow(message: any): MessageActionRow[] {
const nonce = message?.nonce;
if (!nonce) return [];
return [
{
key: "rose-utils-copy-nonce",
label: "Copy Nonce",
icon: COPY_ICON,
onPress: () => copy(String(nonce), "Copied nonce to clipboard"),
},
];
}
function attachmentIdsRow(message: any): MessageActionRow[] {
const attachments = message?.attachments;
if (!Array.isArray(attachments) || !attachments.length) return [];
const ids = attachments.map((a: any) => a?.id).filter(Boolean);
if (!ids.length) return [];
return [
{
key: "rose-utils-copy-attachment-ids",
label: ids.length > 1 ? `Copy Attachment IDs (${ids.length})` : "Copy Attachment ID",
icon: COPY_ICON,
onPress: () => copy(ids.join(", "), `Copied ${ids.length} attachment ID${ids.length === 1 ? "" : "s"} to clipboard`),
},
];
}
function referencedMessageIdRow(message: any): MessageActionRow[] {
const ref = message?.messageReference ?? message?.message_reference;
const refId = ref?.messageId ?? ref?.message_id;
if (!refId) return [];
return [
{
key: "rose-utils-copy-referenced-id",
label: "Copy Replied-to Message ID",
icon: COPY_ICON,
onPress: () => copy(String(refId), "Copied replied-to message ID to clipboard"),
},
];
}
function debugInfoRow(message: any): MessageActionRow[] {
if (!message?.id) return [];
return [
{
key: "rose-utils-copy-debug-info",
label: "Copy Message Debug Info",
sublabel: "Type, flags, and timestamps",
icon: COPY_ICON,
onPress: () => {
const info = {
id: message.id,
channelId: message.channelId ?? message.channel_id,
type: message.type,
flags: message.flags,
timestamp: message.timestamp,
editedTimestamp: message.editedTimestamp ?? message.edited_timestamp,
};
copy(JSON.stringify(info, null, 2), "Copied message debug info to clipboard");
},
},
];
}
export default new Module({
id: "message-dev-tools",
label: "Message Dev Tools",
meta: {
sublabel: "Extra copy-ID rows in the message long-press menu, beyond what Discord's Developer Mode gives you natively",
category: ModuleCategory.Useful,
},
settings: {
copyWebhookId: {
label: "Copy Webhook ID",
subLabel: "For messages sent by a webhook",
type: "toggle",
default: true,
},
copyNonce: {
label: "Copy Nonce",
subLabel: "The client-generated ID used to match an optimistic send to its real message",
type: "toggle",
default: true,
},
copyAttachmentIds: {
label: "Copy Attachment ID(s)",
type: "toggle",
default: true,
},
copyReferencedId: {
label: "Copy Replied-to Message ID",
type: "toggle",
default: true,
},
copyDebugInfo: {
label: "Copy Message Debug Info",
subLabel: "Type, flags, and creation/edit timestamps as JSON",
type: "toggle",
default: true,
},
},
handlers: {
onStart() {
const options = this.storage.options;
if (options.copyWebhookId) this.patches.add(registerMessageAction(webhookIdRow));
if (options.copyNonce) this.patches.add(registerMessageAction(nonceRow));
if (options.copyAttachmentIds) this.patches.add(registerMessageAction(attachmentIdsRow));
if (options.copyReferencedId) this.patches.add(registerMessageAction(referencedMessageIdRow));
if (options.copyDebugInfo) this.patches.add(registerMessageAction(debugInfoRow));
},
onStop() {},
},
});

View file

@ -0,0 +1,131 @@
import { findByStoreName } from "@vendetta/metro";
import { clipboard } from "@vendetta/metro/common";
import { showToast } from "@vendetta/ui/toasts";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { Module, ModuleCategory } from "../../lib/Module";
import { registerMessageAction, type MessageActionRow } from "../../lib/messageActionSheet";
import { decodeSnowflakeTimestamp } from "../../lib/snowflake";
// Confirmed live via Key Inspector Eval (/root/eval-for-revenge/ru/all-checks.txt) against a real
// server: GuildStore.getGuild() returns name/ownerId/features/premiumTier/premiumSubscriberCount/
// verificationLevel/vanityURLCode exactly as read below. RelationshipStore.getFriendIDs() and
// GuildMemberStore.getMember(guildId, userId) are confirmed working too - a real friend showed up
// as a cached member of the test server, cross-checked against UserStore.getUser().username.
const GuildStore = findByStoreName("GuildStore");
const RelationshipStore = findByStoreName("RelationshipStore");
const GuildMemberStore = findByStoreName("GuildMemberStore");
const UserStore = findByStoreName("UserStore");
const COPY_ICON = "ic_copy_24px";
function copy(value: string, toastLabel: string) {
clipboard.setString(value);
showToast(toastLabel, getAssetIDByName(COPY_ICON));
}
function getGuildId(message: any): string | null {
return message?.guildId ?? message?.guild_id ?? null;
}
function serverInfoRow(message: any): MessageActionRow[] {
const guildId = getGuildId(message);
if (!guildId || !GuildStore?.getGuild) return [];
const guild = GuildStore.getGuild(guildId);
if (!guild) return [];
return [
{
key: "rose-utils-copy-server-info",
label: "Copy Server Info",
sublabel: guild.name,
icon: COPY_ICON,
onPress: () => {
const info = {
id: guildId,
name: guild.name,
ownerId: guild.ownerId,
createdAt: decodeSnowflakeTimestamp(guildId)?.toISOString(),
boostTier: guild.premiumTier,
boostCount: guild.premiumSubscriberCount,
verificationLevel: guild.verificationLevel,
vanityURLCode: guild.vanityURLCode,
description: guild.description,
features: guild.features,
};
copy(JSON.stringify(info, null, 2), "Copied server info to clipboard");
},
},
];
}
function friendsInServerRow(message: any): MessageActionRow[] {
const guildId = getGuildId(message);
if (!guildId || !RelationshipStore?.getFriendIDs || !GuildMemberStore?.getMember) return [];
let friendIds: string[];
try {
friendIds = RelationshipStore.getFriendIDs();
} catch {
return [];
}
if (!Array.isArray(friendIds) || !friendIds.length) return [];
return [
{
key: "rose-utils-copy-friends-in-server",
label: "Copy Friends In This Server",
sublabel: "Only counts friends already loaded into this server's member list this session",
icon: COPY_ICON,
onPress: () => {
const inServer = friendIds
.filter((id) => {
try {
return !!GuildMemberStore.getMember(guildId, id);
} catch {
return false;
}
})
.map((id) => UserStore?.getUser?.(id)?.username ?? id);
copy(
inServer.length
? inServer.join(", ")
: "(none found - they may just not be loaded into the member list yet)",
`Copied ${inServer.length} friend${inServer.length === 1 ? "" : "s"} in this server to clipboard`,
);
},
},
];
}
export default new Module({
id: "server-info-tools",
label: "Server Info Tools",
meta: {
sublabel: "Adds Copy Server Info and Copy Friends In This Server rows to the message long-press menu",
category: ModuleCategory.Useful,
},
settings: {
copyServerInfo: {
label: "Copy Server Info",
subLabel: "Boost tier, verification level, features, vanity URL, creation date, and more",
type: "toggle",
default: true,
},
copyFriendsInServer: {
label: "Copy Friends In This Server",
subLabel: "Cross-references your friends list against this server's cached member list",
type: "toggle",
default: true,
},
},
handlers: {
onStart() {
const options = this.storage.options;
if (options.copyServerInfo) this.patches.add(registerMessageAction(serverInfoRow));
if (options.copyFriendsInServer) this.patches.add(registerMessageAction(friendsInServerRow));
},
onStop() {},
},
});

View file

@ -0,0 +1,50 @@
import { clipboard } from "@vendetta/metro/common";
import { showToast } from "@vendetta/ui/toasts";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { Module, ModuleCategory } from "../../lib/Module";
import { decodeSnowflakeTimestamp } from "../../lib/snowflake";
function decodeSnowflake(id: string): string | null {
return decodeSnowflakeTimestamp(id)?.toISOString() ?? null;
}
export default new Module({
id: "snowflake-tools",
label: "Snowflake Tools",
meta: {
sublabel: "Decode any Discord ID's creation timestamp - useful after copying an ID with Developer Mode",
category: ModuleCategory.Useful,
},
settings: {
decodeClipboard: {
label: "Decode ID(s) in clipboard",
subLabel: "Copy one or more Discord IDs first (one per line, or comma-separated), then tap this",
type: "button",
action() {
clipboard.getString().then((text: string) => {
const ids = text.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
if (!ids.length) {
showToast("Clipboard is empty", undefined);
return;
}
const lines = ids.map((id) => {
const decoded = decodeSnowflake(id);
return decoded ? `${id} -> ${decoded}` : `${id} -> not a valid snowflake`;
});
const report = lines.join("\n");
clipboard.setString(report);
showToast(
`Decoded ${lines.length} ID${lines.length === 1 ? "" : "s"} - result copied`,
getAssetIDByName("ic_copy_24px"),
);
});
},
},
},
handlers: {
onStart() {},
onStop() {},
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,65 +0,0 @@
import { findByProps } from "@vendetta/metro";
import { before } from "@vendetta/patcher";
import dark from "./dark.png";
import light from "./light.png";
import { Module, ModuleCategory } from "../../lib/Module";
const MediaManager = findByProps("downloadMediaAsset");
const mediaShareActions = findByProps("useMediaShareActions");
function parseURL(url: string) {
const path = url.split("/");
const tenorIndex = path.findIndex((x) => x.endsWith(".tenor.com"));
if (tenorIndex === -1) return;
const [host, id, file] = path.slice(tenorIndex, tenorIndex + 3);
if (!host || !id || !file) return;
return `https://${host}/${id.slice(0, -2)}AC/${file.split(".")[0]}.gif`;
}
export default new Module({
id: "tenor-gif-fix",
label: "Tenor GIF Fix",
meta: {
sublabel: "Fixes a several-year old bug which downloads gifs from Tenor as videos instead of gif files",
category: ModuleCategory.Fixes,
},
handlers: {
onStart() {
// Older Discord versions.
if (MediaManager?.downloadMediaAsset) {
this.patches.add(
before("downloadMediaAsset", MediaManager, (args: any[]) => {
const url = args[0];
if (!url || typeof url !== "string") return;
const parsed = parseURL(url);
if (parsed) {
args[0] = parsed;
args[1] = 1;
}
}),
);
}
// Newer Discord versions.
if (mediaShareActions?.useMediaShareActions) {
this.patches.add(
before("useMediaShareActions", mediaShareActions, (args: any[]) => {
const source = args[0]?.source;
if (!source?.uri) return;
const parsed = parseURL(source.uri);
if (parsed) {
source.sourceURI = parsed;
delete source.videoURI;
}
}),
);
}
},
onStop() {},
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,11 +1,23 @@
import SpotifyListenAlong from "./SpotifyListenAlong"; import SpotifyListenAlong from "./SpotifyListenAlong";
import TenorGifFix from "./TenorGifFix";
import SendSpotifyInvite from "./SendSpotifyInvite"; import SendSpotifyInvite from "./SendSpotifyInvite";
import Minimod from "./Minimod"; import Minimod from "./Minimod";
import ColorfulChannels from "./ColorfulChannels"; import ColorfulChannels from "./ColorfulChannels";
import EnforceDeveloperMode from "./EnforceDeveloperMode"; import DeveloperMode from "./DeveloperMode";
import MessageDevTools from "./MessageDevTools";
import SnowflakeTools from "./SnowflakeTools";
import ServerInfoTools from "./ServerInfoTools";
// BetterComponents (Material-style Switch/Toast/Alert redesign) isn't ported - it needs // 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 // 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"). // default upstream due to known issues ("the great component functionification of 2025").
export default [SpotifyListenAlong, TenorGifFix, SendSpotifyInvite, Minimod, ColorfulChannels, EnforceDeveloperMode]; // TenorGifFix was dropped from this module list at the user's request.
export default [
SpotifyListenAlong,
SendSpotifyInvite,
Minimod,
ColorfulChannels,
DeveloperMode,
MessageDevTools,
SnowflakeTools,
ServerInfoTools,
];