feat(lib/discord/modules/settings): new method to refresh SettingsOverviewScreen and SettingsNavigator

This commit is contained in:
PalmDevs 2025-07-21 22:40:28 +07:00
parent d2ac578cff
commit a586c0cebd
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
3 changed files with 109 additions and 112 deletions

View file

@ -1,3 +1,4 @@
import { noop } from '@revenge-mod/utils/callback'
import type {
SettingsItem,
SettingsModulesLoadedSubscription,
@ -10,3 +11,10 @@ export const sSections: Record<string, SettingsSection> = {}
export const sConfig: Record<string, SettingsItem> = {}
export const sSubscriptions = new Set<SettingsModulesLoadedSubscription>()
export const sRefresher = {
navigator: false,
overviewScreen: false,
callNavigator: noop,
callOverviewScreen: noop,
}

View file

@ -1,11 +1,6 @@
import { ReactNavigationNative } from '@revenge-mod/externals/react-navigation'
import { noop } from '@revenge-mod/utils/callback'
import { findInTree } from '@revenge-mod/utils/tree'
import { Constants } from '../../common'
import { sLoaded } from '../../start'
import { RootNavigationRef } from '../main_tabs_v2'
import { sConfig, sSections, sSubscriptions } from './_internal'
import type { NavigationState, PartialState } from '@react-navigation/core'
import { sConfig, sRefresher, sSections, sSubscriptions } from './_internal'
import type { DiscordModules } from '../../types'
export type SettingsItem = DiscordModules.Modules.Settings.SettingsItem
@ -101,71 +96,18 @@ export function addSettingsItemToSection(key: string, item: string) {
}
}
const { CommonActions, StackActions } = ReactNavigationNative
/**
* Refreshes the SettingsOverviewScreen.
*/
export function refreshSettingsOverviewScreen() {
sRefresher.overviewScreen = true
sRefresher.callOverviewScreen()
}
/**
* Refreshes the SettingsOverviewScreen, applying any changes made to settings modules.
*
* @param renavigate Whether to renavigate instead of replacing the screen in the stack.
* @returns Whether the SettingsOverviewScreen was refreshed.
* Refreshes the settings navigator.
*/
export async function refreshSettingsOverviewScreen(renavigate?: boolean) {
const navigation = RootNavigationRef.getRootNavigationRef()
if (!navigation.isReady()) return
let state = navigation.getRootState()
// State with SettingsOverviewScreen
let settingsState = findInTree(state, isNavigationSettingsState)
// We're currently not on the settings screen, so we don't need to reset
if (!settingsState) return
if (renavigate) {
const mainState = findInTree(state, isNavigationMainState)
if (!mainState) return
navigation.dispatch({
...CommonActions.goBack(),
target: mainState.key,
})
navigation.navigate(mainState.routes[mainState.index].name)
// Wait for navigation to complete and reset to the settings state
requestAnimationFrame(() => {
navigation.reset({
index: settingsState!.routes.length - 1,
routes: settingsState!.routes,
} as PartialState<NonNullable<typeof settingsState>>)
})
} else {
requestAnimationFrame(() => {
// Get updated state
state = navigation.getRootState()
settingsState = findInTree(state, isNavigationSettingsState)
if (!settingsState) return
const {
key: target,
routes: [{ name, key: source }],
} = settingsState
navigation.dispatch({
...StackActions.replace(name),
source,
target,
})
})
}
}
function isNavigationMainState(state: any): state is NavigationState {
return Array.isArray(state.routes) && state.routes.length > 1
}
function isNavigationSettingsState(state: any): state is NavigationState {
return (
Array.isArray(state.routes) &&
state.routes[0]?.name ===
(Constants.UserSettingsSections as Record<string, string>).OVERVIEW
)
export function refreshSettingsNavigator() {
sRefresher.navigator = true
sRefresher.callNavigator()
}

View file

@ -1,20 +1,22 @@
import { sSections } from '@revenge-mod/discord/_/modules/settings'
import { sRefresher, sSections } from '@revenge-mod/discord/_/modules/settings'
import { onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings'
import { byName } from '@revenge-mod/modules/finders/filters'
import { waitForModules } from '@revenge-mod/modules/finders/wait'
import { after } from '@revenge-mod/patcher'
import {
waitForModuleByImportedPath,
waitForModules,
} from '@revenge-mod/modules/finders/wait'
import { instead } from '@revenge-mod/patcher'
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import { useEffect } from 'react'
import type { FC } from 'react'
import './register-routes'
import { React } from '@revenge-mod/react'
import { noop } from '@revenge-mod/utils/callback'
import { useReRender } from '@revenge-mod/utils/react'
// Always register before so setting modules that depend on the Revenge section can be registered properly
onSettingsModulesLoaded(() => {
require('./register')
})
registerPlugin(
const pluginSettings = registerPlugin(
{
id: 'revenge.settings',
name: 'Settings',
@ -24,43 +26,22 @@ registerPlugin(
},
{
start() {
onSettingsModulesLoaded(() => {
require('./register')
})
waitForModuleByImportedPath(
'modules/main_tabs_v2/native/settings/SettingsNavigator.tsx',
exports => {
patchSettingsNavigator(exports)
},
)
const unsubSOS = waitForModules(
byName('SettingsOverviewScreen'),
exports => {
unsubSOS()
const customSections = sSections
after(exports as { default: FC }, 'default', tree => {
const {
props: { sections },
} = tree as {
props: {
sections: Array<{
label: string
settings: string[]
}>
}
}
// Check if we even have custom sections
const firstCustomSection =
customSections[Object.keys(customSections)[0]]
if (!firstCustomSection) return tree
// Check if sections are already spliced
const [firstCustomItem] = firstCustomSection.settings
if (
sections.findIndex(section =>
section.settings.includes(firstCustomItem),
) < 0
)
for (const section of Object.values(customSections))
if (!section.index) sections.unshift(section)
else sections.splice(section.index, 0, section)
return tree
})
patchSettingsOverviewScreen(exports)
},
{
returnNamespace: true,
@ -71,3 +52,69 @@ registerPlugin(
PluginFlags.Enabled,
InternalPluginFlags.Internal | InternalPluginFlags.Essential,
)
function patchSettingsNavigator(exports: any) {
instead(exports.default, 'type', (args, orig) => {
const reRender = useReRender()
useEffect(() => {
sRefresher.callNavigator = reRender
return () => {
sRefresher.navigator = false
sRefresher.callNavigator = noop
}
}, [reRender])
// useMemo(() => getSettingScreens(), [])
const unpatchMemo = instead(React, 'useMemo', (args, orig) => {
if (!args[1]?.length && sRefresher.navigator) args[1] = undefined
return Reflect.apply(orig, React, args)
})
const fiber = Reflect.apply(orig, undefined, args)
unpatchMemo()
return fiber
})
}
function patchSettingsOverviewScreen(exports: any) {
instead(
exports as {
default: FC
},
'default',
(args, orig) => {
const reRender = useReRender()
useEffect(() => {
sRefresher.callOverviewScreen = reRender
return () => {
sRefresher.overviewScreen = false
sRefresher.callOverviewScreen = noop
}
}, [reRender])
// In: SearchableSettingsList
// useMemo(() => {
// return doSettingsBlocklistFiltering(props.sections, canShowNitroSettings)
// }, [canShowNitroSettings])
const unpatchMemo = instead(React, 'useMemo', (args, orig) => {
if (sRefresher.overviewScreen) args[1] = undefined
const sections = Reflect.apply(orig, React, args)
// Add our custom sections here
for (const section of Object.values(sSections))
if (section.index)
sections.splice(section.index, 0, section)
else sections.unshift(section)
return sections
})
const tree = Reflect.apply(orig, undefined, args)
unpatchMemo()
return tree
},
)
}
export default pluginSettings