feat: improve recovery mode + small lib additions

Improved Recovery Mode features generally:
- Ability to view and modify saved states while not actually running any additional plugins
- Better Recovery Mode banner display
- Enter Recovery Mode option in Revenge Settings UI
- Fixed freeze detection false positives

New APIs:
- lib/discord: `disabled` in `DiscordModules.Components.FloatingActionButtonProps`
- lib/modules/native/app: new submodule with `reloadApp()` method

Fixes:
- Use `reloadApp()` instead of `BundleUpdaterManager.reload()`
This commit is contained in:
PalmDevs 2026-08-01 01:51:29 +07:00
parent 6d7e017c0b
commit 944dbb6838
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
18 changed files with 270 additions and 118 deletions

View file

@ -305,6 +305,7 @@ export namespace DiscordModules {
export interface FloatingActionButtonProps {
icon: number
onPress: () => void
disabled?: boolean
positionBottom?: number
accessibilityLabel?: string
}
@ -513,14 +514,6 @@ export namespace DiscordModules {
export type TableRowIconVariant =
| 'default'
| 'blurple'
| 'boosting-pink'
| 'status-online'
| 'status-idle'
| 'status-dnd'
| 'status-offline'
| 'xbox'
| 'playstation'
| 'danger'
| 'secondary'
| 'translucent'

View file

@ -21,6 +21,9 @@
"./native": {
"default": "./src/native/index.ts"
},
"./native/app": {
"default": "./src/native/app.ts"
},
"./native/fs": {
"default": "./src/native/fs.ts"
},
@ -28,4 +31,4 @@
"types": "./src/types.ts"
}
}
}
}

View file

@ -0,0 +1,11 @@
import { callNativeMethodSync } from '@revenge-mod/modules/native'
export function reloadApp() {
callNativeMethodSync('revenge.app.reload', [])
}
declare module '@revenge-mod/modules/native' {
interface NativeMethods {
'revenge.app.reload': [[], null]
}
}

View file

@ -232,10 +232,29 @@ const pMetadata = new WeakMap<AnyPlugin, InternalPluginMeta>()
/// STATE-SYNC
const { states: InitialPersistedStates }: PersistedPluginStates =
callNativeMethodSync('revenge.plugins.states.read', []) ?? {
states: {},
}
const { states: InitialPersistedStates, savedStates }: PersistedPluginStates =
callNativeMethodSync('revenge.plugins.states.read', [])
/**
* Whether this boot is running with default plugins only, ignoring the user's saved states.
*/
export const isDefaultsOnlyBoot = savedStates != null
/**
* The user's real saved states, only sent when this boot ignores them (recovery/defaults-only).
*
* @see {@link isDefaultsOnlyBoot}
*/
export const SavedPluginStates = savedStates ?? null
/**
* Whether a plugin is enabled in the user's saved setup, which in a defaults-only boot is not
* what's running. Falls back to the session state on a normal boot.
*/
export function isPluginEnabledInSavedStates(plugin: AnyPlugin): boolean {
const saved = SavedPluginStates?.[plugin.manifest.id]
return saved ? Boolean(saved.enabled) : isPluginEnabled(plugin)
}
export function forgetInitialPluginState(id: PluginManifest['id']) {
delete InitialPersistedStates[id]
@ -659,12 +678,41 @@ function tryPreparePluginStart(plugin: AnyPlugin) {
meta.apiLevel = PluginApiLevel.Start
}
/**
* Writes the enabled state of a plugin to the native side, and updates the real saved states.
*/
async function writePluginEnabledState(plugin: AnyPlugin, enabled: boolean) {
const result = await callNativeMethod('revenge.plugins.setEnabled', [
plugin.manifest.id,
enabled,
])
if (result?.code === 'DEPENDENCIES_UNSATISFIED') {
const details = result.problems
.map(
p =>
`"${p.id}" (requires ${p.required}, installed: ${p.installed ?? 'none'}${p.installed && !p.enabled ? ', disabled' : ''})`,
)
.join(', ')
throw new Error(
`Cannot enable plugin "${plugin.manifest.id}": unsatisfied dependencies: ${details}`,
)
}
if (SavedPluginStates)
SavedPluginStates[plugin.manifest.id] = {
...SavedPluginStates[plugin.manifest.id],
enabled,
}
}
/**
* Disables a plugin, as well as all its dependents.
*/
export async function disablePlugin(plugin: AnyPlugin) {
// PendingReload/PendingUpdate never block disabling
if (!isPluginEnabled(plugin))
if (!isPluginEnabledInSavedStates(plugin))
throw new Error(`Plugin "${plugin.manifest.id}" is not enabled`)
const meta = getInternalPluginMeta(plugin)
@ -686,10 +734,7 @@ export async function disablePlugin(plugin: AnyPlugin) {
if (plugin.status && !(plugin.status & Status.Stopping))
await stopPlugin(plugin)
await callNativeMethod('revenge.plugins.setEnabled', [
plugin.manifest.id,
false,
])
await writePluginEnabledState(plugin, false)
meta.flags &= ~Flag.Enabled
pEmitter.emit('disabled', plugin)
@ -699,32 +744,16 @@ export async function disablePlugin(plugin: AnyPlugin) {
* Enables a plugin, as well as all its dependencies.
*/
export async function enablePlugin(plugin: AnyPlugin) {
if (isPluginEnabled(plugin))
if (isPluginEnabledInSavedStates(plugin))
throw new Error(`Plugin "${plugin.manifest.id}" is already enabled`)
await Promise.all(
getPluginDependencies(plugin).map(dep => {
if (!isPluginEnabled(dep)) return enablePlugin(dep)
if (!isPluginEnabledInSavedStates(dep)) return enablePlugin(dep)
}),
)
const result = await callNativeMethod('revenge.plugins.setEnabled', [
plugin.manifest.id,
true,
])
if (result?.code === 'DEPENDENCIES_UNSATISFIED') {
const details = result.problems
.map(
p =>
`"${p.id}" (requires ${p.required}, installed: ${p.installed ?? 'none'}${p.installed && !p.enabled ? ', disabled' : ''})`,
)
.join(', ')
throw new Error(
`Cannot enable plugin "${plugin.manifest.id}": unsatisfied dependencies: ${details}`,
)
}
await writePluginEnabledState(plugin, true)
const meta = getInternalPluginMeta(plugin)
meta.flags |= Flag.Enabled
@ -733,6 +762,11 @@ export async function enablePlugin(plugin: AnyPlugin) {
}
export async function runPluginLate(plugin: AnyPlugin) {
if (isDefaultsOnlyBoot)
throw new Error(
`Cannot start plugin "${plugin.manifest.id}" while running with default plugins. Reload to apply your changes.`,
)
requirePluginStartableState(plugin)
if (plugin.status & Status.Started)
@ -1013,6 +1047,13 @@ export async function deleteStorageForPlugin(plugin: Plugin<any, any>) {
if (await exists(dir)) await rm(dir)
}
export function requestNextBootDefaultsOnly() {
callNativeMethodSync(
'revenge.plugins.states.requestNextBootDefaultsOnly',
[],
)
}
export {
confirmInstall,
resyncPluginSources,
@ -1022,7 +1063,8 @@ export {
declare module '@revenge-mod/modules/native' {
interface NativeMethods {
'revenge.plugins.startNative': [[id: PluginManifest['id']], null]
'revenge.plugins.states.read': [[], PersistedPluginStates | null]
'revenge.plugins.states.read': [[], PersistedPluginStates]
'revenge.plugins.states.requestNextBootDefaultsOnly': [[], void]
'revenge.plugins.setEnabled': [
[id: PluginManifest['id'], enabled: boolean],
SetEnabledError | null,
@ -1055,4 +1097,7 @@ interface PersistedPluginStates {
states: {
[id: PluginManifest['id']]: PluginStateObject
}
savedStates?: {
[id: PluginManifest['id']]: PluginStateObject
}
}

View file

@ -3,6 +3,7 @@ import * as PluginApiModulesFindersFilters from '@revenge-mod/modules/finders/fi
import * as PluginApiModulesMetroSubscriptions from '@revenge-mod/modules/metro/subscriptions'
import * as PluginApiModulesMetroUtils from '@revenge-mod/modules/metro/utils'
import * as PluginApiModulesNative_ from '@revenge-mod/modules/native'
import * as PluginApiModulesNativeApp from '@revenge-mod/modules/native/app'
import * as PluginApiModulesNativeFileSystem from '@revenge-mod/modules/native/fs'
import { spreadDescriptors } from '.'
@ -13,6 +14,7 @@ export interface PluginApiModules {
}
export type PluginApiModulesNative = typeof PluginApiModulesNative_ & {
app: typeof PluginApiModulesNativeApp
fs: typeof PluginApiModulesNativeFileSystem
}
@ -34,6 +36,7 @@ export const modules: PluginApiModules = {
spreadDescriptors(PluginApiModulesMetroSubscriptions, {}),
),
native: spreadDescriptors(PluginApiModulesNative_, {
app: PluginApiModulesNativeApp,
fs: PluginApiModulesNativeFileSystem,
}),
}

View file

@ -13,18 +13,11 @@ export const PluginStatus = {
Stopping: 1 << 6,
}
const { storageRootPath, defaultsOnly } = callNativeMethodSync(
const { storageRootPath } = 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.
*/

View file

@ -276,6 +276,7 @@ function getLibraries() {
['metro/subscriptions', 'metro'],
['metro/utils', 'metro'],
'native',
'native/app',
'native/fs',
'types',
]),

View file

@ -0,0 +1,26 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { reloadApp } from '@revenge-mod/modules/native/app'
import {
isDefaultsOnlyBoot,
requestNextBootDefaultsOnly,
} from '@revenge-mod/plugins/_'
import { Setting } from '~plugins/start/settings/constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeEnterRecoveryModeSetting: SettingsItem = {
parent: Setting.Revenge,
type: 'pressable',
useIsDisabled: () => isDefaultsOnlyBoot,
useTitle: () => 'Enter Recovery Mode',
useDescription: () =>
isDefaultsOnlyBoot
? 'Currently in Recovery Mode. Reload to exit.'
: 'Run Revenge with default plugins only for one boot. Use if plugins are causing issues.',
IconComponent: () => <TableRowAssetIcon name="ShieldIcon" />,
onPress: () => {
requestNextBootDefaultsOnly()
reloadApp()
},
}
export default RevengeEnterRecoveryModeSetting

View file

@ -2,16 +2,25 @@ 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 {
onSettingsModulesLoaded,
registerSettingsItem,
} from '@revenge-mod/discord/modules/settings'
import { callNativeMethod } from '@revenge-mod/modules/native'
import {
InternalPluginFlags,
isDefaultsOnlyBoot,
PluginFlags,
registerInternalPlugin,
} from '@revenge-mod/plugins/_'
import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
import { asap } from '@revenge-mod/utils/callback'
import { AppState } from 'react-native'
import { FullVersion } from '~constants'
import { mErrorChain } from '../../../../lib/modules/src/metro/runtime'
import pluginSettings from '../settings'
import { Setting } from '../settings/constants'
import { RouteNames } from '../settings.plugins/constants'
import RevengeEnterRecoveryModeSetting from './definitions/RevengeEnterRecoveryModeSetting'
registerInternalPlugin(
{
@ -19,28 +28,52 @@ registerInternalPlugin(
name: 'Recovery',
description: 'Provides troubleshooting options.',
author: 'Revenge',
icon: 'WrenchIcon',
icon: 'ShieldIcon',
dependencies: { [pluginSettings]: {} },
},
{
start() {
onSettingsModulesLoaded(() => {
registerSettingsItem(
Setting.RevengeEnterRecoveryMode,
RevengeEnterRecoveryModeSetting,
)
})
// 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)
let currentId: number
const setTimer = () => {
if (currentId) clearTimeout(currentId)
currentId = setTimeout(() => {
if (AppState.currentState !== 'active') return
try {
callNativeMethod('revenge.alertError', [
`App was unable to start. This is likely caused by a plugin.\n\nYou can launch Recovery Mode in the Recovery menu.\n\nErroring modules chain: ${mErrorChain.join(', ') || 'None'}`,
FullVersion,
])
} catch (e) {
console.error(
'Failed to call native method "revenge.alertError":',
e,
)
}
}, 5000)
}
const sub = AppState.addEventListener('change', state => {
if (state === 'active') setTimer()
else clearTimeout(currentId)
})
if (AppState.currentState === 'active') setTimer()
const unsub = onFluxEventDispatched('APP_STATE_UPDATE', e => {
clearTimeout(id)
clearTimeout(currentId)
unsub()
sub.remove()
return e
})
@ -65,7 +98,7 @@ function RecoveryModal() {
<Design.AlertModal
title="Recovery Mode"
content={
'Running with default plugins. Your enabled plugins are saved and will come back when you reload.\n\nUninstall plugins that might be causing issues, then reload the app to get back on track.'
"You are now running with default plugins. Additional plugins can't be started in Recovery Mode.\n\nDisable or uninstall plugins that might be causing issues, then reload the app to exit Recovery Mode."
}
actions={
<>

View file

@ -3,6 +3,8 @@ import { styles } from '@revenge-mod/components/_'
import FormSwitch from '@revenge-mod/components/FormSwitch'
import { Design } from '@revenge-mod/discord/design'
import {
isDefaultsOnlyBoot,
isPluginEnabledInSavedStates,
isPluginEssential,
isPluginPendingUpdate,
isPluginStartable,
@ -12,6 +14,7 @@ import { memo } from 'react'
import { Pressable } from 'react-native'
import { handleDisablePlugin, handleEnablePlugin } from '../utils/actions'
import { openPluginSettings } from '../utils/alerts'
import { messageOf, showErrorToast } from '../utils/repos'
import {
showBrowsePluginActionSheet,
showPluginOptionsActionSheet,
@ -136,6 +139,7 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
meta: InternalPluginMeta
}) {
const enabled = usePluginEnabled(plugin)
const savedEnabled = isPluginEnabledInSavedStates(plugin)
const {
manifest: { name, description, version, author, icon },
@ -208,14 +212,11 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
}}
ref={switchRef}
>
<FormSwitch
key={plugin.manifest.id}
disabled={toggleDisabled}
onValueChange={enabled => {
if (enabled) handleEnablePlugin(plugin)
else handleDisablePlugin(plugin)
}}
value={enabled}
<InstalledPluginSwitch
plugin={plugin}
enabled={enabled}
savedEnabled={savedEnabled}
toggleDisabled={toggleDisabled}
/>
</Pressable>
</>
@ -224,6 +225,32 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
)
})
export const InstalledPluginSwitch = memo(function InstalledPluginSwitch({
plugin,
enabled,
savedEnabled,
toggleDisabled,
}: {
plugin: AnyPlugin
enabled: boolean
savedEnabled: boolean
toggleDisabled: boolean
}) {
return (
<FormSwitch
key={plugin.manifest.id}
disabled={toggleDisabled}
onValueChange={enabled => {
;(enabled
? handleEnablePlugin(plugin)
: handleDisablePlugin(plugin)
).catch(e => showErrorToast(messageOf(e)))
}}
value={isDefaultsOnlyBoot ? savedEnabled : enabled}
/>
)
})
/**
* Card for a plugin that isn't installed yet, shown on the Browse screen.
* No switch or settings button, just a more menu and a small Install button.

View file

@ -53,8 +53,10 @@ export function PluginFlashList({
}
export function InstalledPluginMasonryFlashList({
ListHeaderComponent,
plugins,
}: {
ListHeaderComponent?: React.ComponentType | null
plugins: (readonly [AnyPlugin, InternalPluginMeta])[]
}) {
const numColumns = useNumColumns()
@ -71,6 +73,7 @@ export function InstalledPluginMasonryFlashList({
fadingEdgeLength={16}
keyExtractor={([plugin]) => plugin.manifest.id}
numColumns={numColumns}
ListHeaderComponent={ListHeaderComponent}
ListEmptyComponent={NoPlugins}
renderItem={({ item: [plugin, meta] }) => (
<InstalledPluginCard plugin={plugin} meta={meta} />

View file

@ -1,5 +1,5 @@
import { getAssetIdByName } from '@revenge-mod/assets'
import { FormSwitch, TableRowAssetIcon } from '@revenge-mod/components'
import { TableRowAssetIcon } from '@revenge-mod/components'
import {
ActionSheetActionCreators,
ToastActionCreators,
@ -12,6 +12,8 @@ import {
getPluginDependencies,
getPluginDependents,
InternalPluginFlags,
isDefaultsOnlyBoot,
isPluginEnabledInSavedStates,
isPluginEssential,
isPluginInternal,
isPluginPendingUpdate,
@ -27,14 +29,13 @@ import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import { useEffect, useState } from 'react'
import { Pressable } from 'react-native'
import { ClickOutsideProvider } from 'react-native-click-outside'
import { handleDisablePlugin, handleEnablePlugin } from '../utils/actions'
import {
openPluginSettings,
showPluginClearDataConfirmation,
showPluginUninstallConfirmation,
} from '../utils/alerts'
import { messageOf, showErrorToast } from '../utils/repos'
import { PluginInfo } from './PluginCard'
import { InstalledPluginSwitch, PluginInfo } from './PluginCard'
import { usePluginEnabled, usePluginStatus } from './PluginStateProvider'
import {
EnablePluginTooltipProvider,
@ -77,13 +78,13 @@ export default function PluginOptionsActionSheet({
icon={icon}
actions={
!essential && (
<FormSwitch
value={enabled}
disabled={pendingUpdate}
onValueChange={v => {
if (v) handleEnablePlugin(plugin)
else handleDisablePlugin(plugin)
}}
<InstalledPluginSwitch
enabled={enabled}
plugin={plugin}
savedEnabled={isPluginEnabledInSavedStates(
plugin,
)}
toggleDisabled={pendingUpdate}
/>
)
}
@ -291,7 +292,8 @@ function PluginActions({
size="lg"
icon={running ? StopIcon : PlayIcon}
label={running ? 'Stop' : 'Start'}
disabled={!running && !startable}
// Nothing can start in a defaults-only boot, stopping a default plugin is still fine
disabled={!running && (!startable || isDefaultsOnlyBoot)}
onPress={async () => {
try {
if (running) await stopPlugin(plugin)

View file

@ -1,5 +1,5 @@
import { Design } from '@revenge-mod/discord/design'
import { BundleUpdaterManager } from '@revenge-mod/discord/native'
import { reloadApp } from '@revenge-mod/modules/native/app'
import type { AnyPlugin } from '@revenge-mod/plugins/_'
const { AlertModal, AlertActionButton, Text } = Design
@ -36,8 +36,7 @@ export default function PluginsRequireReloadAlert({
variant="destructive"
text="Reload"
onPress={() => {
// TODO: Replace with bridge call
BundleUpdaterManager.reload()
reloadApp()
}}
/>
<AlertActionButton variant="secondary" text="Not now" />

View file

@ -1,6 +1,10 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { isPluginEnabled, pEmitter, pList } from '@revenge-mod/plugins/_'
import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
import {
isDefaultsOnlyBoot,
isPluginEnabled,
pEmitter,
pList,
} from '@revenge-mod/plugins/_'
import { useReRender } from '@revenge-mod/utils/react'
import { useEffect } from 'react'
import { RouteNames, Setting } from '../constants'

View file

@ -5,10 +5,11 @@ 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 { reloadApp } from '@revenge-mod/modules/native/app'
import {
getInternalPluginMeta,
InternalPluginFlags,
isDefaultsOnlyBoot,
isPluginEnabled,
isPluginEssential,
isPluginInternal,
@ -17,7 +18,6 @@ import {
pEmitter,
pList,
} from '@revenge-mod/plugins/_'
import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
import { debounce } from '@revenge-mod/utils/callback'
import {
useCallback,
@ -26,7 +26,7 @@ import {
useMemo,
useState,
} from 'react'
import { View } from 'react-native'
import { Image, View } from 'react-native'
import { ClickOutsideProvider } from 'react-native-click-outside'
import RevengeIcon from '~assets/RevengeIcon'
import { InstalledPluginMasonryFlashList } from '../components/PluginList'
@ -137,11 +137,12 @@ function HeaderButton() {
)
}
function BrowseFloatingActionButton() {
function BrowseFloatingActionButton({ disabled }: { disabled?: boolean }) {
const navigation = useNavigation<NavigationProp<any>>()
return (
<FloatingActionButton
disabled={disabled}
icon={PlusLargeIcon}
accessibilityLabel="Browse plugins"
onPress={() =>
@ -151,34 +152,34 @@ function BrowseFloatingActionButton() {
)
}
function DefaultsOnlyBanner() {
const [hide, setHide] = useState(false)
if (!isDefaultsOnlyBoot || hide) return null
function RecoveryModeBanner() {
return (
<Card>
<Stack spacing={16}>
<Card style={{ marginHorizontal: 6, marginVertical: 6, boxShadow: '' }}>
<Stack spacing={12}>
<Stack direction="horizontal" spacing={8} align="center">
<Image
source={getAssetIdByName('ShieldIcon')!}
style={{
width: 18,
height: 18,
}}
/>
<Text variant="text-md/semibold">Recovery Mode</Text>
</Stack>
<Text variant="text-sm/medium">
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.
You are now running with default plugins. Additional plugins
can't be started in Recovery Mode.{'\n\n'}
Disable or uninstall plugins that might be causing issues,
then reload the app to exit Recovery Mode.
</Text>
<Stack spacing={8}>
<Design.Button
icon={getAssetIdByName('RetryIcon')!}
size="sm"
text="Reload with all plugins enabled"
onPress={() => BundleUpdaterManager.reload()}
/>
{/* In case it's taking too much vertical space */}
<Design.Button
icon={getAssetIdByName('EyeSlashIcon')!}
size="sm"
variant="secondary"
text="Hide this message"
onPress={() => setHide(true)}
text="Exit Recovery Mode"
onPress={() => {
reloadApp()
}}
/>
</Stack>
</Stack>
@ -263,7 +264,6 @@ function Screen() {
return (
<>
<DefaultsOnlyBanner />
<Stack direction="horizontal">
<View style={styles.grow}>
<SearchInput onChange={debouncedSetSearch} size="md" />
@ -294,8 +294,13 @@ function Screen() {
}
/>
</Stack>
<InstalledPluginMasonryFlashList plugins={plugins} />
<BrowseFloatingActionButton />
<InstalledPluginMasonryFlashList
ListHeaderComponent={
isDefaultsOnlyBoot ? RecoveryModeBanner : null
}
plugins={plugins}
/>
<BrowseFloatingActionButton disabled={isDefaultsOnlyBoot} />
</>
)
}

View file

@ -9,6 +9,7 @@ export const Setting = {
RevengeSourceRepository: 'RevengeSourceRepository',
RevengeLicense: 'RevengeLicense',
Reload: 'Reload',
RevengeEnterRecoveryMode: 'RevengeEnterRecoveryMode',
RevengeVersion: 'RevengeVersion',
ReactVersion: 'ReactVersion',

View file

@ -1,5 +1,5 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { BundleUpdaterManager } from '@revenge-mod/discord/native'
import { reloadApp } from '@revenge-mod/modules/native/app'
import { Setting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
@ -8,7 +8,7 @@ const ReloadSetting: SettingsItem = {
IconComponent: () => <TableRowAssetIcon name="RetryIcon" />,
useTitle: () => 'Reload App',
onPress: () => {
BundleUpdaterManager.reload()
reloadApp()
},
type: 'pressable',
}

View file

@ -27,7 +27,10 @@ export default function RevengeSettingScreen() {
},
{
label: 'Actions',
settings: [Setting.Reload],
settings: [
Setting.Reload,
Setting.RevengeEnterRecoveryMode,
],
},
],
}}