From 53af7ce78549405072ff41809e6bf64d4f803c3d Mon Sep 17 00:00:00 2001
From: everestmcarthur <280397636+everestmcarthur@users.noreply.github.com>
Date: Sat, 1 Aug 2026 11:55:27 -0400
Subject: [PATCH] ViewRaw: adopt the ActionSheetRow technique from
MoveForwardButton/FastCopyUserID
---
plugins/view-raw/src/index.tsx | 200 ++++++++++++++++++---------------
1 file changed, 107 insertions(+), 93 deletions(-)
diff --git a/plugins/view-raw/src/index.tsx b/plugins/view-raw/src/index.tsx
index 15d0673..4e271e0 100644
--- a/plugins/view-raw/src/index.tsx
+++ b/plugins/view-raw/src/index.tsx
@@ -5,9 +5,14 @@
// - Fixed a crash on every message long-press ("Cannot read property 'type' of undefined") caused
// by assuming Discord's ActionSheetRow icon shape without checking it first.
// - RawPage rebuilt with a search box, syntax-highlighted JSON, and clearer copy actions.
-// - Added a third, name-independent fallback for finding the action sheet's row list, plus a
-// diagnostics record (visible in Settings) of which strategy matched - the original had exactly
-// two shape-specific strategies, and gave up silently if neither matched a given Discord build.
+// - Action-sheet insertion rewritten using the technique from fshinz/Revenge-Plugins'
+// MoveForwardButton/FastCopyUserID: build the button from the real ActionSheetRow component
+// (via findByProps("ActionSheetRow"), with its own .Icon/.Group) instead of fabricating a fake
+// icon element by copying $$typeof/type off a sibling - and fall back to creating a brand new
+// group if no existing group can be found, rather than giving up.
+// - Guards against re-patching the same action sheet instance twice (Discord appears to reuse the
+// same lazy-loaded instance across opens), and against a missing Navigator lookup.
+// - Diagnostics (visible in Settings) record which strategy matched, or why none did.
import { before, after } from "@vendetta/patcher";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { findInReactTree } from "@vendetta/utils";
@@ -25,127 +30,132 @@ const modalCloseButton =
findByProps("getHeaderCloseButton")?.getHeaderCloseButton;
const Navigator =
findByName("Navigator") ?? findByProps("Navigator")?.Navigator;
+const { ActionSheetRow } = findByProps("ActionSheetRow") ?? {};
const { FormRow, FormIcon } = Forms;
-function viewRawFormRow(navigator: () => JSX.Element) {
- return (
-
- }
- onPress={() => {
- LazyActionSheet.hideActionSheet();
- Navigation.push(navigator);
+function buildNavigator(getMessage: () => any) {
+ return () => (
+ Navigation.pop()),
+ render: () => ,
+ },
}}
/>
);
}
-const unpatch = before("openLazy", LazyActionSheet, ([component, key, msg]) => {
- const message = msg?.message;
- if (key !== "MessageLongPressActionSheet" || !message) return;
- component.then((instance) => {
- const unpatch = after("default", instance, (_, component) => {
- React.useEffect(
- () => () => {
- unpatch();
- },
- [],
- );
+function openRawPage(navigator: () => JSX.Element) {
+ LazyActionSheet.hideActionSheet();
+ Navigation.push(navigator);
+}
- // Every branch below pokes at Discord's internal action-sheet component shape, which isn't a
- // stable API - wrapped so a shape mismatch just skips adding the button instead of crashing
- // the long-press menu for every message.
+function formRowButton(navigator: () => JSX.Element) {
+ return (
+
+ }
+ onPress={() => openRawPage(navigator)}
+ />
+ );
+}
+
+function actionSheetRowButton(navigator: () => JSX.Element) {
+ return (
+ }
+ onPress={() => openRawPage(navigator)}
+ key="view-raw"
+ />
+ );
+}
+
+const patches: (() => void)[] = [];
+
+const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, msg]) => {
+ if (key !== "MessageLongPressActionSheet" || !msg?.message) return;
+
+ component.then((instance: any) => {
+ // Discord appears to reuse the same lazy-loaded instance across every action sheet open, so
+ // this only patches it once and tracks the active message on the instance itself - patching on
+ // every open would stack duplicate buttons (and duplicate patches) after enough long-presses.
+ instance.__viewRawActiveMessage = msg.message;
+ if (instance.__viewRawPatched) return;
+ instance.__viewRawPatched = true;
+
+ const unpatchDefault = after("default", instance, (_: any, component: any) => {
try {
- const navigator = () => (
- Navigation.pop()),
- render: () => ,
- },
- }}
- />
- );
+ if (!Navigator) {
+ console.log("[ViewRaw] Error: Navigator not found - can't open RawPage");
+ recordDetection("none", "Navigator not found");
+ return;
+ }
+
+ const navigator = buildNavigator(() => instance.__viewRawActiveMessage);
// Strategy 1: a plain list of buttons (older/simpler action sheet layouts).
- const buttons = findInReactTree(
- component,
- (x) => x?.[0]?.type?.name === "ButtonRow",
- );
-
- // Strategy 2: the row-group layout, where we build a fake icon element to match the
- // existing rows' internal shape.
- const actionSheetContainer = findInReactTree(
- component,
- (x) => Array.isArray(x) && x[0]?.type?.name === "ActionSheetRowGroup",
- );
-
+ const buttons = findInReactTree(component, (x) => x?.[0]?.type?.name === "ButtonRow");
if (buttons) {
- buttons.push(viewRawFormRow(navigator));
+ buttons.push(formRowButton(navigator));
recordDetection("buttons");
return;
}
- if (actionSheetContainer && actionSheetContainer[1]) {
- const middleGroup = actionSheetContainer[1];
- const firstChild = middleGroup?.props?.children?.[0];
- const ActionSheetRow = firstChild?.type;
- const iconProps = firstChild?.props?.icon;
+ // Strategy 2: real ActionSheetRow groups. Search every group for one that already has
+ // ActionSheetRow-shaped children, insert there - and if none match, create a brand new
+ // group at the top rather than giving up (this is the actual difference from before).
+ const groups = findInReactTree(
+ component,
+ (x) => Array.isArray(x) && x[0]?.type?.name === "ActionSheetRowGroup",
+ );
- if (ActionSheetRow && iconProps && Array.isArray(middleGroup?.props?.children)) {
- const viewRawButton = (
- (
-
- ),
- },
- }}
- onPress={() => {
- LazyActionSheet.hideActionSheet();
- Navigation.push(navigator);
- }}
- key="view-raw"
- />
+ if (Array.isArray(groups) && groups.length && ActionSheetRow) {
+ const button = actionSheetRowButton(navigator);
+ let inserted = false;
+
+ for (const group of groups) {
+ const children = findInReactTree(
+ group,
+ (c) => Array.isArray(c) && c.some((child: any) => child?.type?.name === "ActionSheetRow"),
);
+ if (Array.isArray(children)) {
+ children.push(button);
+ inserted = true;
+ break;
+ }
+ }
- middleGroup.props.children.push(viewRawButton);
+ if (!inserted && typeof groups.unshift === "function" && ActionSheetRow.Group) {
+ groups.unshift({button});
+ inserted = true;
+ }
+
+ if (inserted) {
recordDetection("actionSheetRow");
return;
}
}
- // Strategy 3: name-independent fallback. Rather than matching a specific component name
- // (which changes across Discord builds/minification), look for any array where every
- // element already looks like an action sheet row - has both a label and an onPress. This
- // is a shape signature, not a name, so it survives renames that break strategies 1 and 2.
+ // Strategy 3: name-independent fallback. Look for any array where every element already
+ // looks like an action sheet row (has both a label and an onPress) - a shape signature
+ // rather than a name, so it survives renames that break strategies 1 and 2.
const genericRowGroup = findInReactTree(
component,
(x) =>
Array.isArray(x) &&
x.length > 0 &&
- x.every((el) => typeof el?.props?.label === "string" && typeof el?.props?.onPress === "function"),
+ x.every((el: any) => typeof el?.props?.label === "string" && typeof el?.props?.onPress === "function"),
);
if (Array.isArray(genericRowGroup)) {
- genericRowGroup.push(viewRawFormRow(navigator));
+ genericRowGroup.push(formRowButton(navigator));
recordDetection("generic");
return;
}
@@ -157,10 +167,14 @@ const unpatch = before("openLazy", LazyActionSheet, ([component, key, msg]) => {
recordDetection("none", String(e));
}
});
+
+ patches.push(unpatchDefault);
});
});
+patches.push(unpatchOpenLazy);
+
export default {
- onUnload: () => unpatch(),
+ onUnload: () => patches.forEach((unpatch) => unpatch()),
settings: Settings,
};