feat(src/plugins/settings): use global state context to reduce rerenders
This commit is contained in:
parent
6852c816d0
commit
d2ac578cff
5 changed files with 117 additions and 42 deletions
|
|
@ -2,14 +2,14 @@ import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { useState } from 'react'
|
||||
import type { AssetId } from '@revenge-mod/assets/types'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
||||
|
||||
export interface FilterAndSortActionSheetProps {
|
||||
filters: Record<
|
||||
string,
|
||||
{
|
||||
icon: AssetId
|
||||
filter: (p: AnyPlugin, iflags: number) => boolean
|
||||
filter: (p: AnyPlugin, meta: InternalPluginMeta) => boolean
|
||||
}
|
||||
>
|
||||
filter: string[]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { styles } from '@revenge-mod/components/_'
|
|||
import FormSwitch from '@revenge-mod/components/FormSwitch'
|
||||
import { Tokens } from '@revenge-mod/discord/common'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { RootNavigationRef } from '@revenge-mod/discord/modules/main_tabs_v2'
|
||||
import { ReactNavigationNative } from '@revenge-mod/externals/react-navigation'
|
||||
import {
|
||||
enablePlugin,
|
||||
InternalPluginFlags,
|
||||
|
|
@ -11,8 +11,10 @@ import {
|
|||
preInitPlugin,
|
||||
startPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { memo } from 'react'
|
||||
import { Image, Pressable } from 'react-native'
|
||||
import { useClickOutside } from 'react-native-click-outside'
|
||||
import { usePluginEnabled } from './PluginStateProvider'
|
||||
import {
|
||||
enableTooltipTarget,
|
||||
essentialTooltipTarget,
|
||||
|
|
@ -20,7 +22,7 @@ import {
|
|||
setEnablePluginTooltipVisible,
|
||||
setEssentialPluginTooltipVisible,
|
||||
} from './TooltipProvider'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
||||
import type { View } from 'react-native'
|
||||
|
||||
const { Card, Text, Stack, IconButton, createStyles } = Design
|
||||
|
|
@ -28,22 +30,23 @@ const { Card, Text, Stack, IconButton, createStyles } = Design
|
|||
const PuzzlePieceIcon = getAssetIdByName('PuzzlePieceIcon', 'png')!
|
||||
const SettingsIcon = getAssetIdByName('SettingsIcon', 'png')!
|
||||
|
||||
export function InstalledPluginCard({
|
||||
export const InstalledPluginCard = memo(function InstalledPluginCard({
|
||||
plugin,
|
||||
iflags,
|
||||
enabled,
|
||||
meta,
|
||||
rightGap,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
iflags: number
|
||||
enabled: boolean
|
||||
meta: InternalPluginMeta
|
||||
rightGap?: boolean
|
||||
}) {
|
||||
const navigation = ReactNavigationNative.useNavigation()
|
||||
const enabled = usePluginEnabled(plugin)
|
||||
|
||||
const {
|
||||
manifest: { name, description, author, icon },
|
||||
} = plugin
|
||||
|
||||
const essential = Boolean(iflags & InternalPluginFlags.Essential)
|
||||
const essential = Boolean(meta.iflags & InternalPluginFlags.Essential)
|
||||
const styles_ = usePluginCardStyles()
|
||||
|
||||
const settingsRef = useClickOutside<View>(() => {
|
||||
|
|
@ -94,9 +97,7 @@ export function InstalledPluginCard({
|
|||
variant="secondary"
|
||||
icon={SettingsIcon}
|
||||
onPress={() => {
|
||||
RootNavigationRef.getRootNavigationRef().navigate(
|
||||
plugin.manifest.id,
|
||||
)
|
||||
navigation.navigate(plugin.manifest.id)
|
||||
}}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
|
|
@ -146,7 +147,7 @@ export function InstalledPluginCard({
|
|||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const usePluginCardStyles = createStyles({
|
||||
icon: {
|
||||
|
|
|
|||
77
src/plugins/init/settings/components/PluginStateProvider.tsx
Normal file
77
src/plugins/init/settings/components/PluginStateProvider.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export interface PluginEnabledStates {
|
||||
[pluginId: string]: boolean
|
||||
}
|
||||
|
||||
const PluginStatesContext = createContext<{
|
||||
enabled: PluginEnabledStates
|
||||
} | null>(null)
|
||||
|
||||
export default function PluginStatesProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
const [enabled, setEnabled] = useState<PluginEnabledStates>(() => {
|
||||
const states: PluginEnabledStates = {}
|
||||
for (const plugin of pList.values()) {
|
||||
states[plugin.manifest.id] = Boolean(
|
||||
plugin.flags & PluginFlags.Enabled,
|
||||
)
|
||||
}
|
||||
return states
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleEnabled = (plugin: AnyPlugin) => {
|
||||
setEnabled(prev => ({
|
||||
...prev,
|
||||
[plugin.manifest.id]: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDisabled = (plugin: AnyPlugin) => {
|
||||
setEnabled(prev => ({
|
||||
...prev,
|
||||
[plugin.manifest.id]: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pEmitter.on('enabled', handleEnabled)
|
||||
pEmitter.on('disabled', handleDisabled)
|
||||
|
||||
return () => {
|
||||
pEmitter.off('enabled', handleEnabled)
|
||||
pEmitter.off('disabled', handleDisabled)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
enabled: enabled,
|
||||
}),
|
||||
[enabled],
|
||||
)
|
||||
|
||||
return (
|
||||
<PluginStatesContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</PluginStatesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function usePluginEnabled(plugin: AnyPlugin) {
|
||||
const context = useContext(PluginStatesContext)
|
||||
|
||||
if (!context) {
|
||||
console.warn('usePluginEnabled not used within a PluginStateProvider')
|
||||
return Boolean(plugin.flags & PluginFlags.Enabled)
|
||||
}
|
||||
|
||||
return context.enabled[plugin.manifest.id]
|
||||
}
|
||||
|
|
@ -15,10 +15,7 @@ const RevengePluginsSetting: SettingsItem = {
|
|||
type: 'route',
|
||||
IconComponent: () => <TableRowAssetIcon name="PuzzlePieceIcon" />,
|
||||
title: () => 'Plugins',
|
||||
useTrailing: () => {
|
||||
useUpdateOnPluginStatesChange()
|
||||
return `${enabledCount} enabled`
|
||||
},
|
||||
useTrailing: () => `${useEnabledPluginCount()} enabled`,
|
||||
screen: {
|
||||
route: RouteNames[Setting.RevengePlugins],
|
||||
getComponent: () =>
|
||||
|
|
@ -39,7 +36,7 @@ pEmitter.on('enabled', () => {
|
|||
enabledCount++
|
||||
})
|
||||
|
||||
export function useUpdateOnPluginStatesChange() {
|
||||
function useEnabledPluginCount() {
|
||||
const reRender = useReRender()
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -51,6 +48,8 @@ export function useUpdateOnPluginStatesChange() {
|
|||
pEmitter.off('enabled', reRender)
|
||||
}
|
||||
}, [reRender])
|
||||
|
||||
return enabledCount
|
||||
}
|
||||
|
||||
const { AlertActionButton, AlertModal, Text } = Design
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@ import { useWindowDimensions, View } from 'react-native'
|
|||
import { ClickOutsideProvider } from 'react-native-click-outside'
|
||||
import RevengeIcon from '~assets/RevengeIcon'
|
||||
import { InstalledPluginCard } from '../components/PluginCard'
|
||||
import PluginStatesProvider from '../components/PluginStateProvider'
|
||||
import {
|
||||
EnablePluginTooltipProvider,
|
||||
EssentialPluginTooltipProvider,
|
||||
resetTooltips,
|
||||
} from '../components/TooltipProvider'
|
||||
import { useUpdateOnPluginStatesChange } from '../definitions/RevengePluginsSetting'
|
||||
import type { RouteProp } from '@react-navigation/core'
|
||||
import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
||||
import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet'
|
||||
import type { RouteNames, Setting } from '../constants'
|
||||
|
||||
|
|
@ -34,13 +34,15 @@ export default function RevengePluginsSettingScreen() {
|
|||
return (
|
||||
<LayerScope>
|
||||
<ClickOutsideProvider>
|
||||
<Page spacing={16}>
|
||||
<EssentialPluginTooltipProvider>
|
||||
<EnablePluginTooltipProvider>
|
||||
<Screen />
|
||||
</EnablePluginTooltipProvider>
|
||||
</EssentialPluginTooltipProvider>
|
||||
</Page>
|
||||
<PluginStatesProvider>
|
||||
<Page spacing={16}>
|
||||
<EssentialPluginTooltipProvider>
|
||||
<EnablePluginTooltipProvider>
|
||||
<Screen />
|
||||
</EnablePluginTooltipProvider>
|
||||
</EssentialPluginTooltipProvider>
|
||||
</Page>
|
||||
</PluginStatesProvider>
|
||||
</ClickOutsideProvider>
|
||||
</LayerScope>
|
||||
)
|
||||
|
|
@ -63,7 +65,8 @@ const Filters: FilterAndSortActionSheetProps['filters'] = {
|
|||
},
|
||||
Essential: {
|
||||
icon: getAssetIdByName('StarIcon')!,
|
||||
filter: (_, iflags) => Boolean(iflags & InternalPluginFlags.Essential),
|
||||
filter: (_, meta) =>
|
||||
Boolean(meta.iflags & InternalPluginFlags.Essential),
|
||||
},
|
||||
} satisfies FilterAndSortActionSheetProps['filters']
|
||||
const DefaultFilters: FilterAndSortActionSheetProps['filter'] = []
|
||||
|
|
@ -82,8 +85,6 @@ const Sorts = {
|
|||
} satisfies FilterAndSortActionSheetProps['sorts']
|
||||
|
||||
function Screen() {
|
||||
useUpdateOnPluginStatesChange()
|
||||
|
||||
const navigation = ReactNavigationNative.useNavigation()
|
||||
const route =
|
||||
ReactNavigationNative.useRoute<
|
||||
|
|
@ -108,18 +109,18 @@ function Screen() {
|
|||
const allPlugins = useMemo(
|
||||
() =>
|
||||
[...pList.values()].map(
|
||||
plugin => [plugin, pMetadata.get(plugin)!.iflags] as const,
|
||||
plugin => [plugin, pMetadata.get(plugin)!] as const,
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
const plugins = allPlugins
|
||||
.filter(([plugin, iflags]) => {
|
||||
.filter(([plugin, meta]) => {
|
||||
if (filter.length === 0) return true
|
||||
if (matchAll)
|
||||
return filter.every(f => Filters[f].filter(plugin, iflags))
|
||||
return filter.every(f => Filters[f].filter(plugin, meta))
|
||||
|
||||
return filter.some(f => Filters[f].filter(plugin, iflags))
|
||||
return filter.some(f => Filters[f].filter(plugin, meta))
|
||||
})
|
||||
.filter(([plugin]) => {
|
||||
const { name, description, author } = plugin.manifest
|
||||
|
|
@ -132,8 +133,7 @@ function Screen() {
|
|||
})
|
||||
.sort(([a], [b]) => {
|
||||
const result = Sorts[sort as keyof typeof Sorts][1](a, b)
|
||||
if (reverse) return -result
|
||||
return result
|
||||
return reverse ? -result : result
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
@ -190,7 +190,7 @@ function Screen() {
|
|||
function PluginMasonryFlashList({
|
||||
plugins,
|
||||
}: {
|
||||
plugins: (readonly [AnyPlugin, number])[]
|
||||
plugins: (readonly [AnyPlugin, InternalPluginMeta])[]
|
||||
}) {
|
||||
const { width, height } = useWindowDimensions()
|
||||
const numColumns = Math.floor((width - 16) / 448)
|
||||
|
|
@ -205,12 +205,10 @@ function PluginMasonryFlashList({
|
|||
estimatedItemSize={116}
|
||||
numColumns={numColumns}
|
||||
ListEmptyComponent={NoPlugins}
|
||||
renderItem={({ item: [plugin, iflags], columnIndex }) => (
|
||||
renderItem={({ item: [plugin, meta], columnIndex }) => (
|
||||
<InstalledPluginCard
|
||||
key={plugin.manifest.id}
|
||||
enabled={Boolean(plugin.flags & PluginFlags.Enabled)}
|
||||
iflags={iflags}
|
||||
plugin={plugin}
|
||||
meta={meta}
|
||||
rightGap={columnIndex + 1 < numColumns}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue