From 8f74dfc79cad5dc958959322e2cbd76c92d687be Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Mon, 5 May 2025 19:13:01 +0700 Subject: [PATCH] feat(plugins/settings): add react devtools support --- lib/types/types.d.ts | 2 +- src/plugins/settings/SettingsConstants.ts | 4 + .../ReactDevToolsConnectSetting.tsx | 21 +++++ .../ReactDevToolsDisconnectSetting.tsx | 19 +++++ .../ReactDevToolsVersionSetting.tsx | 18 ++++ src/plugins/settings/devtools.ts | 84 +++++++++++++++++++ src/plugins/settings/register.ts | 6 ++ .../screens/RevengeDeveloperSettingScreen.tsx | 53 +++++++++--- 8 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 src/plugins/settings/definitions/ReactDevToolsConnectSetting.tsx create mode 100644 src/plugins/settings/definitions/ReactDevToolsDisconnectSetting.tsx create mode 100644 src/plugins/settings/definitions/ReactDevToolsVersionSetting.tsx create mode 100644 src/plugins/settings/devtools.ts diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 4744fc3..2713c5d 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -47,7 +47,7 @@ declare global { declare global { var __REACT_DEVTOOLS_GLOBAL_HOOK__: unknown | undefined - var __reactDevTools: + var __REACT_DEVTOOLS__: | { version: string exports: { diff --git a/src/plugins/settings/SettingsConstants.ts b/src/plugins/settings/SettingsConstants.ts index 0b9ab2e..61dba9e 100644 --- a/src/plugins/settings/SettingsConstants.ts +++ b/src/plugins/settings/SettingsConstants.ts @@ -8,6 +8,7 @@ export const MobileSetting = { REVENGE_DEVELOPER: 'REVENGE_DEVELOPER', // SUBSETTINGS + REVENGE_DISCORD_SERVER: 'REVENGE_DISCORD_SERVER', REVENGE_GITHUB_ORGANIZATION: 'REVENGE_GITHUB_ORGANIZATION', RELOAD_APP: 'RELOAD_APP', @@ -17,6 +18,9 @@ export const MobileSetting = { REACT_NATIVE_VERSION: 'REACT_NATIVE_VERSION', HERMES_VERSION: 'HERMES_VERSION', + REACT_DEVTOOLS_VERSION: 'REACT_DEVTOOLS_VERSION', + REACT_DEVTOOLS_CONNECT: 'REACT_DEVTOOLS_CONNECT', + REACT_DEVTOOLS_DISCONNECT: 'REACT_DEVTOOLS_DISCONNECT', EVALUATE_JAVASCRIPT: 'EVALUATE_JAVASCRIPT', REVENGE_NOT_IMPLEMENTED: 'REVENGE_NOT_IMPLEMENTED', diff --git a/src/plugins/settings/definitions/ReactDevToolsConnectSetting.tsx b/src/plugins/settings/definitions/ReactDevToolsConnectSetting.tsx new file mode 100644 index 0000000..1b32e19 --- /dev/null +++ b/src/plugins/settings/definitions/ReactDevToolsConnectSetting.tsx @@ -0,0 +1,21 @@ +import { React } from '@revenge-mod/react' + +import AssetIcon from '../components/icons/AssetIcon' +import { connectToDevTools, DevToolsContext, useIsDevToolsOpen } from '../devtools' +import { MobileSetting } from '../SettingsConstants' + +import type { SettingsItem } from '@revenge-mod/discord/ui/settings' + +const ReactDevToolsConnectSetting: SettingsItem = { + parent: MobileSetting.REVENGE_DEVELOPER, + IconComponent: () => , + title: () => 'Connect to React DevTools', + usePredicate: () => { + const dtOpen = useIsDevToolsOpen() + return DevToolsContext.available && !dtOpen + }, + onPress: connectToDevTools, + type: 'pressable', +} + +export default ReactDevToolsConnectSetting diff --git a/src/plugins/settings/definitions/ReactDevToolsDisconnectSetting.tsx b/src/plugins/settings/definitions/ReactDevToolsDisconnectSetting.tsx new file mode 100644 index 0000000..7491c0a --- /dev/null +++ b/src/plugins/settings/definitions/ReactDevToolsDisconnectSetting.tsx @@ -0,0 +1,19 @@ +import { React } from '@revenge-mod/react' + +import AssetIcon from '../components/icons/AssetIcon' +import { disconnectFromDevTools, useIsDevToolsOpen } from '../devtools' +import { MobileSetting } from '../SettingsConstants' + +import type { SettingsItem } from '@revenge-mod/discord/ui/settings' + +const ReactDevToolsDisconnectSetting: SettingsItem = { + parent: MobileSetting.REVENGE_DEVELOPER, + IconComponent: () => , + variant: 'danger', + title: () => 'Disconnect from React DevTools', + usePredicate: useIsDevToolsOpen, + onPress: disconnectFromDevTools, + type: 'pressable', +} + +export default ReactDevToolsDisconnectSetting diff --git a/src/plugins/settings/definitions/ReactDevToolsVersionSetting.tsx b/src/plugins/settings/definitions/ReactDevToolsVersionSetting.tsx new file mode 100644 index 0000000..9c2d807 --- /dev/null +++ b/src/plugins/settings/definitions/ReactDevToolsVersionSetting.tsx @@ -0,0 +1,18 @@ +import { React } from '@revenge-mod/react' + +import AssetIcon from '../components/icons/AssetIcon' +import { MobileSetting } from '../SettingsConstants' + +import type { SettingsItem } from '@revenge-mod/discord/ui/settings' +import { DevToolsContext } from '../devtools' + +const ReactDevToolsVersionSetting: SettingsItem = { + parent: MobileSetting.REVENGE_DEVELOPER, + IconComponent: () => , + title: () => 'React DevTools', + useDescription: () => __REACT_DEVTOOLS__!.version, + usePredicate: () => DevToolsContext.available, + type: 'static', +} + +export default ReactDevToolsVersionSetting diff --git a/src/plugins/settings/devtools.ts b/src/plugins/settings/devtools.ts new file mode 100644 index 0000000..1e67d05 --- /dev/null +++ b/src/plugins/settings/devtools.ts @@ -0,0 +1,84 @@ +import { getAssetByName } from '@revenge-mod/assets' +import { ToastActionCreators } from '@revenge-mod/discord/actions' +import { React } from '@revenge-mod/react' +import { getErrorStack } from '@revenge-mod/utils/errors' +import { useReRender } from '@revenge-mod/utils/react' +import { pluginApi } from '.' + +export const DevToolsContext: { + ws: WebSocket | undefined + address: string + open: boolean + available: boolean +} = { + available: Boolean(globalThis.__REACT_DEVTOOLS__), + ws: undefined, + address: 'localhost:8097', + open: false, +} + +type Subscription = (event: 1 | 2 | 3, err?: unknown) => void +const subscriptions: Set = new Set() + +subscriptions.add((e, err) => { + if (e === 3) { + const actualError = (err as { message: string }).message ?? getErrorStack(err) + + pluginApi.logger.error('React DevTools error:', actualError) + + ToastActionCreators.open({ + key: 'DEVTOOLS_ERROR', + icon: getAssetByName('CircleXIcon-primary')!.id, + content: actualError, + }) + } +}) + +export function connectToDevTools() { + if (!DevToolsContext.available || DevToolsContext.ws) return + + const websocket = (DevToolsContext.ws = new WebSocket(`ws://${DevToolsContext.address}`)) + + websocket.addEventListener('open', () => { + DevToolsContext.open = true + for (const sub of subscriptions) sub(1) + }) + + websocket.addEventListener('close', () => { + cleanup() + for (const sub of subscriptions) sub(2) + }) + + websocket.addEventListener('error', e => { + cleanup() + for (const sub of subscriptions) sub(3, e) + }) + + __REACT_DEVTOOLS__!.exports.connectToDevTools({ + websocket, + }) +} + +function cleanup() { + DevToolsContext.open = false + DevToolsContext.ws = undefined +} + +export function disconnectFromDevTools() { + if (DevToolsContext.ws) DevToolsContext.ws.close() +} + +export function useIsDevToolsOpen() { + const rerender = useReRender() + + React.useEffect(() => { + const sub: Subscription = e => (e === 1 || e === 2) && rerender() + subscriptions.add(sub) + + return () => { + subscriptions.delete(sub) + } + }, []) + + return DevToolsContext.open +} diff --git a/src/plugins/settings/register.ts b/src/plugins/settings/register.ts index a96d19b..7ef52e8 100644 --- a/src/plugins/settings/register.ts +++ b/src/plugins/settings/register.ts @@ -13,6 +13,9 @@ import RevengeVersionSetting from './definitions/RevengeVersionSetting' import HermesVersionSetting from './definitions/HermesVersionSetting' import RevengeNotImplementedSetting from './definitions/RevengeNotImplementedSetting' import EvaluateJavaScriptSetting from './definitions/EvaluateJavaScriptSetting' +import ReactDevToolsVersionSetting from './definitions/ReactDevToolsVersionSetting' +import ReactDevToolsConnectSetting from './definitions/ReactDevToolsConnectSetting' +import ReactDevToolsDisconnectSetting from './definitions/ReactDevToolsDisconnectSetting' import ReloadAppSetting from './definitions/ReloadAppSetting' import RevengeDiscordServerSetting from './definitions/RevengeDiscordServerSetting' import RevengeGitHubOrganizationSetting from './definitions/RevengeGitHubOrganizationSetting' @@ -30,6 +33,9 @@ registerSettingsItems({ [MobileSetting.REACT_VERSION]: ReactVersionSetting, [MobileSetting.REACT_NATIVE_VERSION]: ReactNativeVersionSetting, [MobileSetting.HERMES_VERSION]: HermesVersionSetting, + [MobileSetting.REACT_DEVTOOLS_VERSION]: ReactDevToolsVersionSetting, + [MobileSetting.REACT_DEVTOOLS_CONNECT]: ReactDevToolsConnectSetting, + [MobileSetting.REACT_DEVTOOLS_DISCONNECT]: ReactDevToolsDisconnectSetting, [MobileSetting.EVALUATE_JAVASCRIPT]: EvaluateJavaScriptSetting, [MobileSetting.REVENGE_NOT_IMPLEMENTED]: RevengeNotImplementedSetting, }) diff --git a/src/plugins/settings/screens/RevengeDeveloperSettingScreen.tsx b/src/plugins/settings/screens/RevengeDeveloperSettingScreen.tsx index 5e9a737..a50c038 100644 --- a/src/plugins/settings/screens/RevengeDeveloperSettingScreen.tsx +++ b/src/plugins/settings/screens/RevengeDeveloperSettingScreen.tsx @@ -1,20 +1,49 @@ +import { React, ReactNative } from '@revenge-mod/react' +import { Components } from '@revenge-mod/discord/ui' import { SettingListRenderer } from '@revenge-mod/discord/ui/settings' import { MobileSetting } from '../SettingsConstants' +import { DevToolsContext } from '../devtools' +// TODO(plugins/settings): debug bridge export default function RevengeDeveloperSettingScreen() { return ( - + + {globalThis.__REACT_DEVTOOLS__ && } + + + ) +} + +export function ReactDevToolsAddressSetting() { + return ( + + (DevToolsContext.address = text)} + returnKeyType="done" + /> + ) }