From 66465552bbbadfd4246fc4fbfc4ad39c220b8d6a Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 12 Aug 2026 18:02:56 +0700 Subject: [PATCH] fix(plugins/settings.plugins): essential plugin tooltip showing up for pending updates + refactor tooltips --- .../components/PluginCard.tsx | 32 +-- .../components/PluginList.tsx | 14 +- .../components/PluginOptionsActionSheet.tsx | 92 +++++---- .../components/TooltipProvider.tsx | 193 ++++++++++-------- .../RevengePluginsBrowseSettingScreen.tsx | 24 +-- .../screens/RevengePluginsSettingScreen.tsx | 20 +- 6 files changed, 189 insertions(+), 186 deletions(-) diff --git a/src/plugins/start/settings.plugins/components/PluginCard.tsx b/src/plugins/start/settings.plugins/components/PluginCard.tsx index c593dd3..7e4ed3d 100644 --- a/src/plugins/start/settings.plugins/components/PluginCard.tsx +++ b/src/plugins/start/settings.plugins/components/PluginCard.tsx @@ -21,11 +21,7 @@ import { } from '../utils/sheets' import { PluginIcon } from './PluginIcon' import { usePluginEnabled } from './PluginStateProvider' -import { - useClickOutsideTooltip, - useEnablePluginTooltip, - useEssentialPluginTooltip, -} from './TooltipProvider' +import { PluginTooltip, usePluginTooltip } from './TooltipProvider' import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_' const { Card, Text, Stack, IconButton, Button, createStyles } = Design @@ -151,14 +147,12 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({ const toggleDisabled = essential || pendingUpdate - const enableTooltip = useEnablePluginTooltip() - const essentialTooltip = useEssentialPluginTooltip() + const [settingsRef, showEnableTooltip] = usePluginTooltip( + PluginTooltip.Enable, + ) - const settingsRef = useClickOutsideTooltip(useEnablePluginTooltip, () => {}) - - const switchRef = useClickOutsideTooltip( - useEssentialPluginTooltip, - () => {}, + const [switchRef, showToggleTooltip] = usePluginTooltip( + essential ? PluginTooltip.Essential : PluginTooltip.PendingUpdate, ) return ( @@ -181,12 +175,7 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({ {plugin.SettingsComponent && ( { - if (!startable) - requestAnimationFrame(() => { - enableTooltip.targetRef.current = - settingsRef.current - enableTooltip.setVisible(true) - }) + if (!startable) showEnableTooltip() }} > { - if (toggleDisabled) - requestAnimationFrame(() => { - essentialTooltip.targetRef.current = - switchRef.current - essentialTooltip.setVisible(true) - }) + if (toggleDisabled) showToggleTooltip() }} ref={switchRef} > diff --git a/src/plugins/start/settings.plugins/components/PluginList.tsx b/src/plugins/start/settings.plugins/components/PluginList.tsx index 47b0990..44e4050 100644 --- a/src/plugins/start/settings.plugins/components/PluginList.tsx +++ b/src/plugins/start/settings.plugins/components/PluginList.tsx @@ -9,7 +9,7 @@ import { PLUGIN_CARD_HALF_GUTTER, PluginCard, } from './PluginCard' -import { useResetTooltips } from './TooltipProvider' +import { useHidePluginTooltips } from './TooltipProvider' import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_' import type { RepoPluginListing } from '@revenge-mod/plugins/_/repositories' import type { FlashListProps } from '@shopify/flash-list' @@ -25,14 +25,14 @@ export function PluginFlashList({ FlashListProps, 'onContentSizeChange' >) { - const resetTooltips = useResetTooltips() + const hideTooltips = useHidePluginTooltips() return ( plugin.manifest.id} renderItem={({ @@ -60,7 +60,7 @@ export function InstalledPluginMasonryFlashList({ plugins: (readonly [AnyPlugin, InternalPluginMeta])[] }) { const numColumns = useNumColumns() - const resetTooltips = useResetTooltips() + const hideTooltips = useHidePluginTooltips() return ( plugin.manifest.id} numColumns={numColumns} @@ -124,7 +124,7 @@ export function BrowsePluginMasonryFlashList({ onInstall: (entry: BrowseEntry) => void }) { const numColumns = useNumColumns() - const resetTooltips = useResetTooltips() + const hideTooltips = useHidePluginTooltips() return ( entry.key} numColumns={numColumns} diff --git a/src/plugins/start/settings.plugins/components/PluginOptionsActionSheet.tsx b/src/plugins/start/settings.plugins/components/PluginOptionsActionSheet.tsx index 87998c1..bd7b8ab 100644 --- a/src/plugins/start/settings.plugins/components/PluginOptionsActionSheet.tsx +++ b/src/plugins/start/settings.plugins/components/PluginOptionsActionSheet.tsx @@ -37,10 +37,9 @@ import { import { messageOf, showErrorToast } from '../utils/repos' import { InstalledPluginSwitch, PluginInfo } from './PluginCard' import { usePluginEnabled, usePluginStatus } from './PluginStateProvider' -import { - EnablePluginTooltipProvider, - useClickOutsideTooltip, - useEnablePluginTooltip, +import PluginTooltipsProvider, { + PluginTooltip, + usePluginTooltip, } from './TooltipProvider' import type { AnyPlugin } from '@revenge-mod/plugins/_' @@ -61,23 +60,44 @@ export default function PluginOptionsActionSheet({ plugin, sheetKey, }: PluginOptionsActionSheetProps) { + return ( + + + + + + + + ) +} + +function PluginOptions({ plugin, sheetKey }: PluginOptionsActionSheetProps) { const enabled = usePluginEnabled(plugin) const meta = getInternalPluginMeta(plugin) const essential = isPluginEssential(meta) const pendingUpdate = isPluginPendingUpdate(plugin) const { name, author, description, icon, version } = plugin.manifest + const [switchRef, showPendingUpdateTooltip] = usePluginTooltip( + PluginTooltip.PendingUpdate, + ) + return ( - - - + { + if (pendingUpdate) showPendingUpdateTooltip() + }} + ref={switchRef} + > - ) - } - /> - - - { - ActionSheetActionCreators.hideActionSheet( - sheetKey, - ) - }} - /> - - - - - - + + ) + } + /> + { + ActionSheetActionCreators.hideActionSheet(sheetKey) + }} + /> + + + ) } @@ -272,8 +286,9 @@ function PluginActions({ plugin: AnyPlugin closeSheet: () => void }) { - const enableTooltip = useEnablePluginTooltip() - const settingsRef = useClickOutsideTooltip(useEnablePluginTooltip, () => {}) + const [settingsRef, showEnableTooltip] = usePluginTooltip( + PluginTooltip.Enable, + ) const meta = getInternalPluginMeta(plugin) const startable = isPluginStartable(plugin) const enabled = usePluginEnabled(plugin) @@ -327,12 +342,7 @@ function PluginActions({ {plugin.SettingsComponent && ( { - if (!startable) - requestAnimationFrame(() => { - enableTooltip.targetRef.current = - settingsRef.current - enableTooltip.setVisible(true) - }) + if (!startable) showEnableTooltip() }} > - setVisible: (visible: boolean) => void +export const PluginTooltip = { + /** Shown when an action requires the plugin to be enabled first. */ + Enable: 'enable', + /** Shown when an action is blocked because the plugin is essential. */ + Essential: 'essential', + /** Shown when an action is blocked because the plugin has a pending update. */ + PendingUpdate: 'pendingUpdate', +} as const + +export type PluginTooltip = (typeof PluginTooltip)[keyof typeof PluginTooltip] + +const Labels: Record = { + [PluginTooltip.Enable]: 'Plugin must be enabled first', + [PluginTooltip.Essential]: 'Plugin needed for Revenge to function properly', + [PluginTooltip.PendingUpdate]: + 'Plugin has a pending update, reload to apply it', } -const EnablePluginTooltipContext = createContext( - null, -) -const EssentialPluginTooltipContext = createContext( +interface PluginTooltipsContextValue { + show: (tooltip: PluginTooltip, target: View | null) => void + hide: () => void +} + +const PluginTooltipsContext = createContext( null, ) -export function EnablePluginTooltipProvider({ +export default function PluginTooltipsProvider({ children, }: { children: React.ReactNode }) { - const [visible, setVisible] = useState(false) - const targetRef = useRef(null) + const [visible, setVisible] = useState(null) - useTooltip(targetRef, { - label: 'Plugin must be enabled first', + // Targets are assigned by `show`, right before the tooltip becomes visible + const enableTarget = useRef(null) + const essentialTarget = useRef(null) + const pendingUpdateTarget = useRef(null) + + useTooltip(enableTarget, { + label: Labels[PluginTooltip.Enable], position: 'top', - visible, + visible: visible === PluginTooltip.Enable, }) - return ( - - {children} - - ) -} - -export function EssentialPluginTooltipProvider({ - children, -}: { - children: React.ReactNode -}) { - const [visible, setVisible] = useState(false) - const targetRef = useRef(null) - - useTooltip(targetRef, { - label: 'Plugin needed for Revenge to function properly', + useTooltip(essentialTarget, { + label: Labels[PluginTooltip.Essential], position: 'top', - visible, + visible: visible === PluginTooltip.Essential, }) - return ( - - {children} - - ) -} + useTooltip(pendingUpdateTarget, { + label: Labels[PluginTooltip.PendingUpdate], + position: 'top', + visible: visible === PluginTooltip.PendingUpdate, + }) -export function useEnablePluginTooltip() { - const context = useContext(EnablePluginTooltipContext) - if (!context) { - throw new Error( - 'useEnablePluginTooltip must be used within EnablePluginTooltipProvider', - ) - } - return context -} - -export function useEssentialPluginTooltip() { - const context = useContext(EssentialPluginTooltipContext) - if (!context) { - throw new Error( - 'useEssentialPluginTooltip must be used within EssentialPluginTooltipProvider', - ) - } - return context -} - -export function useResetTooltips() { - const enableTooltip = useContext(EnablePluginTooltipContext) - const essentialTooltip = useContext(EssentialPluginTooltipContext) - - return () => { - enableTooltip?.setVisible(false) - essentialTooltip?.setVisible(false) - } -} - -export function useClickOutsideTooltip( - contextHook: () => TooltipContextValue, - onClickOutside: () => void, -) { - const { targetRef, setVisible } = contextHook() - - const ref = useClickOutside(() => { - setVisible(false) - if (targetRef.current === ref.current) { - onClickOutside() + const value = useMemo(() => { + const targets = { + [PluginTooltip.Enable]: enableTarget, + [PluginTooltip.Essential]: essentialTarget, + [PluginTooltip.PendingUpdate]: pendingUpdateTarget, } - }) - return ref + return { + show: (tooltip, target) => { + targets[tooltip].current = target + setVisible(tooltip) + }, + hide: () => setVisible(null), + } + }, []) + + return ( + + + {children} + + + ) +} + +/** + * Binds an element to a plugin tooltip. + * + * @param tooltip The tooltip to show for the element. + * @returns The ref to attach to the element, and a callback showing the tooltip on it. + */ +export function usePluginTooltip(tooltip: PluginTooltip) { + const context = useContext(PluginTooltipsContext) + if (!context) + throw new Error( + 'usePluginTooltip must be used within PluginTooltipsProvider', + ) + + const { show, hide } = context + + // Tapping anywhere else dismisses the tooltip + const ref = useClickOutside(hide) + + const showTooltip = useCallback(() => { + // Tapping the element itself also counts as a tap outside of it (the + // press target wraps the ref), so showing waits for the next frame + requestAnimationFrame(() => { + show(tooltip, ref.current) + }) + }, [show, tooltip, ref]) + + return [ref, showTooltip] as const +} + +/** Hides the visible plugin tooltip, if any.. */ +export function useHidePluginTooltips() { + const context = useContext(PluginTooltipsContext) + + return useCallback(() => context?.hide(), [context]) } diff --git a/src/plugins/start/settings.plugins/screens/RevengePluginsBrowseSettingScreen.tsx b/src/plugins/start/settings.plugins/screens/RevengePluginsBrowseSettingScreen.tsx index 6018f38..20a9625 100644 --- a/src/plugins/start/settings.plugins/screens/RevengePluginsBrowseSettingScreen.tsx +++ b/src/plugins/start/settings.plugins/screens/RevengePluginsBrowseSettingScreen.tsx @@ -13,13 +13,9 @@ import { import { debounce } from '@revenge-mod/utils/callback' import { useCallback, useEffect, useMemo, useState } from 'react' import { View } from 'react-native' -import { ClickOutsideProvider } from 'react-native-click-outside' import { BrowsePluginMasonryFlashList } from '../components/PluginList' import PluginStatesProvider from '../components/PluginStateProvider' -import { - EnablePluginTooltipProvider, - EssentialPluginTooltipProvider, -} from '../components/TooltipProvider' +import PluginTooltipsProvider from '../components/TooltipProvider' import { runInstallFlow } from '../utils/repos' import type { RepoPluginListing } from '@revenge-mod/plugins/_/repositories' import type { BrowseSortKey } from '../components/BrowseFilterAndSortActionSheet' @@ -34,17 +30,13 @@ const SearchDebounceTime = 100 export default function RevengePluginsBrowseSettingScreen() { return ( - - - - - - - - - - - + + + + + + + ) } diff --git a/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx b/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx index 505c335..b984dcf 100644 --- a/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx +++ b/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx @@ -28,14 +28,10 @@ import { useState, } from 'react' import { Image, View } from 'react-native' -import { ClickOutsideProvider } from 'react-native-click-outside' import RevengeIcon from '~assets/RevengeIcon' import { InstalledPluginMasonryFlashList } from '../components/PluginList' import PluginStatesProvider from '../components/PluginStateProvider' -import { - EnablePluginTooltipProvider, - EssentialPluginTooltipProvider, -} from '../components/TooltipProvider' +import PluginTooltipsProvider from '../components/TooltipProvider' import { RouteNames, Setting } from '../constants' import type { NavigationProp, RouteProp } from '@react-navigation/core' import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation' @@ -51,17 +47,13 @@ const PlusLargeIcon = getAssetIdByName('PlusLargeIcon')! export default function RevengePluginsSettingScreen() { return ( - - + + - - - - - + - - + + ) }