From 1497e8ef8d3b3e8ccb7aac3f61d3c5cf8c932cd7 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Fri, 31 Jul 2026 16:43:55 +0700 Subject: [PATCH] feat: app freeze detection and recovery mode This requires loader support and can only be triggered from a native dialog. Recovery mode toggle won't be a functionality for now. If there's a use, such as binary searching problematic plugins, then it'll be added (similar to VSCode extension bisect). --- lib/plugins/src/constants.ts | 9 +- lib/plugins/src/types.ts | 5 +- src/plugins/start/recovery/index.tsx | 90 +++++++++++++++++++ .../definitions/RevengePluginsSetting.tsx | 5 +- .../screens/RevengePluginsSettingScreen.tsx | 41 ++++++++- 5 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 src/plugins/start/recovery/index.tsx diff --git a/lib/plugins/src/constants.ts b/lib/plugins/src/constants.ts index b7c746a..7b71f77 100644 --- a/lib/plugins/src/constants.ts +++ b/lib/plugins/src/constants.ts @@ -13,11 +13,18 @@ export const PluginStatus = { Stopping: 1 << 6, } -const { storageRootPath } = callNativeMethodSync( +const { storageRootPath, defaultsOnly } = callNativeMethodSync( 'revenge.plugins.getConstants', [], ) +/** + * Whether this boot ignores saved plugin states. + * + * Only essential and enabled-by-default plugins run. Saved states will come back on the next reload. + */ +export const isDefaultsOnlyBoot = defaultsOnly + /** * Absolute path to per-plugin storage directory. */ diff --git a/lib/plugins/src/types.ts b/lib/plugins/src/types.ts index 68f6f40..31759ec 100644 --- a/lib/plugins/src/types.ts +++ b/lib/plugins/src/types.ts @@ -321,6 +321,9 @@ export interface PluginSettingsComponent< declare module '@revenge-mod/modules/native' { export interface NativeMethods { - 'revenge.plugins.getConstants': [[], { storageRootPath: string }] + 'revenge.plugins.getConstants': [ + [], + { storageRootPath: string; defaultsOnly: boolean }, + ] } } diff --git a/src/plugins/start/recovery/index.tsx b/src/plugins/start/recovery/index.tsx new file mode 100644 index 0000000..cc9bf39 --- /dev/null +++ b/src/plugins/start/recovery/index.tsx @@ -0,0 +1,90 @@ +import { AlertActionCreators } from '@revenge-mod/discord/actions' +import { Design } from '@revenge-mod/discord/design' +import { onFluxEventDispatched } from '@revenge-mod/discord/flux' +import { RootNavigationRef } from '@revenge-mod/discord/modules/main_tabs_v2' +import { callNativeMethod } from '@revenge-mod/modules/native' +import { + InternalPluginFlags, + PluginFlags, + registerInternalPlugin, +} from '@revenge-mod/plugins/_' +import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants' +import { asap } from '@revenge-mod/utils/callback' +import { FullVersion } from '~constants' +import { RouteNames } from '../settings.plugins/constants' + +registerInternalPlugin( + { + id: 'revenge.recovery', + name: 'Recovery', + description: 'Provides troubleshooting options.', + author: 'Revenge', + icon: 'WrenchIcon', + }, + { + start() { + // Freeze detection + const id = setTimeout(() => { + try { + callNativeMethod('revenge.alertError', [ + 'Application seems frozen. This is likely caused by a plugin.\n\nYou can launch with default plugins in the Recovery menu.', + FullVersion, + ]) + } catch (e) { + console.error( + 'Failed to call native method "revenge.alertError":', + e, + ) + } + }, 10000) + + const unsub = onFluxEventDispatched('APP_STATE_UPDATE', e => { + clearTimeout(id) + unsub() + return e + }) + + if (isDefaultsOnlyBoot) { + asap(() => { + AlertActionCreators.openAlert( + 'revenge-recovery', + , + ) + }) + } + }, + }, + PluginFlags.Enabled, + InternalPluginFlags.Internal | InternalPluginFlags.Essential, +) + +function RecoveryModal() { + const navigation = RootNavigationRef.getRootNavigationRef() + + return ( + + { + // React Navigation will bubble our requests to the main navigator if we're already in settings + navigation.navigate('settings', { + screen: RouteNames.RevengePlugins, + }) + }} + /> + + + } + /> + ) +} diff --git a/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx b/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx index 96576ae..4dc4c4f 100644 --- a/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx +++ b/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx @@ -1,5 +1,6 @@ import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon' import { isPluginEnabled, pEmitter, pList } from '@revenge-mod/plugins/_' +import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants' import { useReRender } from '@revenge-mod/utils/react' import { useEffect } from 'react' import { RouteNames, Setting } from '../constants' @@ -11,7 +12,9 @@ const RevengePluginsSetting: SettingsItem = { type: 'route', IconComponent: () => , useTitle: () => 'Plugins', - useTrailing: () => `${useEnabledPluginCount()} enabled`, + useTrailing: () => + `${useEnabledPluginCount()} enabled` + + (isDefaultsOnlyBoot ? ' (recovery)' : ''), screen: { route: RouteNames[Setting.RevengePlugins], getComponent: () => RevengePluginsSettingScreen.default, diff --git a/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx b/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx index 8beb392..cdfce26 100644 --- a/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx +++ b/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx @@ -5,6 +5,7 @@ import Page from '@revenge-mod/components/Page' import SearchInput from '@revenge-mod/components/SearchInput' import { ActionSheetActionCreators } from '@revenge-mod/discord/actions' import { Design } from '@revenge-mod/discord/design' +import { BundleUpdaterManager } from '@revenge-mod/discord/native' import { getInternalPluginMeta, InternalPluginFlags, @@ -16,6 +17,7 @@ import { pEmitter, pList, } from '@revenge-mod/plugins/_' +import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants' import { debounce } from '@revenge-mod/utils/callback' import { useCallback, @@ -38,7 +40,8 @@ import type { NavigationProp, RouteProp } from '@react-navigation/core' import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation' import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet' -const { Stack, IconButton, FloatingActionButton, LayerScope } = Design +const { Stack, IconButton, FloatingActionButton, LayerScope, Card, Text } = + Design const FiltersHorizontalIcon = getAssetIdByName('FiltersHorizontalIcon', 'png')! const SettingsIcon = getAssetIdByName('SettingsIcon')! @@ -148,6 +151,41 @@ function BrowseFloatingActionButton() { ) } +function DefaultsOnlyBanner() { + const [hide, setHide] = useState(false) + + if (!isDefaultsOnlyBoot || hide) return null + + return ( + + + + Running with default plugins. Your enabled plugins are saved + and will come back when you reload.{'\n\n'} + Uninstall plugins that might be causing issues, then reload + the app to get back on track. + + + BundleUpdaterManager.reload()} + /> + {/* In case it's taking too much vertical space */} + setHide(true)} + /> + + + + ) +} + function snapshotPlugins() { return [...pList.values()].map( plugin => [plugin, getInternalPluginMeta(plugin)] as const, @@ -225,6 +263,7 @@ function Screen() { return ( <> +