fix(plugins/settings.plugins): essential plugin tooltip showing up for pending updates + refactor tooltips
This commit is contained in:
parent
fc41d45f2a
commit
66465552bb
6 changed files with 189 additions and 186 deletions
|
|
@ -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 && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (!startable)
|
||||
requestAnimationFrame(() => {
|
||||
enableTooltip.targetRef.current =
|
||||
settingsRef.current
|
||||
enableTooltip.setVisible(true)
|
||||
})
|
||||
if (!startable) showEnableTooltip()
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
|
|
@ -203,12 +192,7 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
)}
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (toggleDisabled)
|
||||
requestAnimationFrame(() => {
|
||||
essentialTooltip.targetRef.current =
|
||||
switchRef.current
|
||||
essentialTooltip.setVisible(true)
|
||||
})
|
||||
if (toggleDisabled) showToggleTooltip()
|
||||
}}
|
||||
ref={switchRef}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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<AnyPlugin>,
|
||||
'onContentSizeChange'
|
||||
>) {
|
||||
const resetTooltips = useResetTooltips()
|
||||
const hideTooltips = useHidePluginTooltips()
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
style={gutterCompensation}
|
||||
onContentSizeChange={onContentSizeChange}
|
||||
data={plugins}
|
||||
onScrollBeginDrag={resetTooltips}
|
||||
onScrollBeginDrag={hideTooltips}
|
||||
fadingEdgeLength={plugins.length === 1 ? 0 : 16}
|
||||
keyExtractor={plugin => 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 (
|
||||
<FlashList
|
||||
|
|
@ -69,7 +69,7 @@ export function InstalledPluginMasonryFlashList({
|
|||
// FAB is 56px tall, plus 16px spacing on top and bottom
|
||||
contentContainerStyle={{ paddingBottom: 56 + 2 * 16 }}
|
||||
data={plugins}
|
||||
onScrollBeginDrag={resetTooltips}
|
||||
onScrollBeginDrag={hideTooltips}
|
||||
fadingEdgeLength={16}
|
||||
keyExtractor={([plugin]) => 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 (
|
||||
<FlashList
|
||||
|
|
@ -132,7 +132,7 @@ export function BrowsePluginMasonryFlashList({
|
|||
style={gutterCompensation}
|
||||
contentContainerStyle={{ paddingBottom: 16 }}
|
||||
data={entries}
|
||||
onScrollBeginDrag={resetTooltips}
|
||||
onScrollBeginDrag={hideTooltips}
|
||||
fadingEdgeLength={16}
|
||||
keyExtractor={entry => entry.key}
|
||||
numColumns={numColumns}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ActionSheet>
|
||||
<ClickOutsideProvider>
|
||||
<PluginTooltipsProvider>
|
||||
<PluginOptions plugin={plugin} sheetKey={sheetKey} />
|
||||
</PluginTooltipsProvider>
|
||||
</ClickOutsideProvider>
|
||||
</ActionSheet>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<ActionSheet>
|
||||
<Stack spacing={24} style={{ paddingTop: 8 }}>
|
||||
<PluginInfo
|
||||
name={name}
|
||||
author={author}
|
||||
version={formatVersion(version)}
|
||||
description={description}
|
||||
icon={icon}
|
||||
actions={
|
||||
!essential && (
|
||||
<Stack spacing={24} style={{ paddingTop: 8 }}>
|
||||
<PluginInfo
|
||||
name={name}
|
||||
author={author}
|
||||
version={formatVersion(version)}
|
||||
description={description}
|
||||
icon={icon}
|
||||
actions={
|
||||
!essential && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (pendingUpdate) showPendingUpdateTooltip()
|
||||
}}
|
||||
ref={switchRef}
|
||||
>
|
||||
<InstalledPluginSwitch
|
||||
enabled={enabled}
|
||||
plugin={plugin}
|
||||
|
|
@ -86,25 +106,19 @@ export default function PluginOptionsActionSheet({
|
|||
)}
|
||||
toggleDisabled={pendingUpdate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ClickOutsideProvider>
|
||||
<EnablePluginTooltipProvider>
|
||||
<PluginActions
|
||||
plugin={plugin}
|
||||
closeSheet={() => {
|
||||
ActionSheetActionCreators.hideActionSheet(
|
||||
sheetKey,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</EnablePluginTooltipProvider>
|
||||
</ClickOutsideProvider>
|
||||
<StatusSection plugin={plugin} />
|
||||
<AdvancedSection plugin={plugin} />
|
||||
</Stack>
|
||||
</ActionSheet>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<PluginActions
|
||||
plugin={plugin}
|
||||
closeSheet={() => {
|
||||
ActionSheetActionCreators.hideActionSheet(sheetKey)
|
||||
}}
|
||||
/>
|
||||
<StatusSection plugin={plugin} />
|
||||
<AdvancedSection plugin={plugin} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -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 && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (!startable)
|
||||
requestAnimationFrame(() => {
|
||||
enableTooltip.targetRef.current =
|
||||
settingsRef.current
|
||||
enableTooltip.setVisible(true)
|
||||
})
|
||||
if (!startable) showEnableTooltip()
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
|
|
|
|||
|
|
@ -1,109 +1,134 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { createContext, useContext, useRef, useState } from 'react'
|
||||
import { useClickOutside } from 'react-native-click-outside'
|
||||
import type { RefObject } from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
ClickOutsideProvider,
|
||||
useClickOutside,
|
||||
} from 'react-native-click-outside'
|
||||
import type { View } from 'react-native'
|
||||
|
||||
const { useTooltip } = Design
|
||||
|
||||
interface TooltipContextValue {
|
||||
targetRef: RefObject<View | null>
|
||||
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, string> = {
|
||||
[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<TooltipContextValue | null>(
|
||||
null,
|
||||
)
|
||||
const EssentialPluginTooltipContext = createContext<TooltipContextValue | null>(
|
||||
interface PluginTooltipsContextValue {
|
||||
show: (tooltip: PluginTooltip, target: View | null) => void
|
||||
hide: () => void
|
||||
}
|
||||
|
||||
const PluginTooltipsContext = createContext<PluginTooltipsContextValue | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
export function EnablePluginTooltipProvider({
|
||||
export default function PluginTooltipsProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const targetRef = useRef<View | null>(null)
|
||||
const [visible, setVisible] = useState<PluginTooltip | null>(null)
|
||||
|
||||
useTooltip(targetRef, {
|
||||
label: 'Plugin must be enabled first',
|
||||
// Targets are assigned by `show`, right before the tooltip becomes visible
|
||||
const enableTarget = useRef<View | null>(null)
|
||||
const essentialTarget = useRef<View | null>(null)
|
||||
const pendingUpdateTarget = useRef<View | null>(null)
|
||||
|
||||
useTooltip(enableTarget, {
|
||||
label: Labels[PluginTooltip.Enable],
|
||||
position: 'top',
|
||||
visible,
|
||||
visible: visible === PluginTooltip.Enable,
|
||||
})
|
||||
|
||||
return (
|
||||
<EnablePluginTooltipContext.Provider value={{ targetRef, setVisible }}>
|
||||
{children}
|
||||
</EnablePluginTooltipContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function EssentialPluginTooltipProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const targetRef = useRef<View | null>(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 (
|
||||
<EssentialPluginTooltipContext.Provider
|
||||
value={{ targetRef, setVisible }}
|
||||
>
|
||||
{children}
|
||||
</EssentialPluginTooltipContext.Provider>
|
||||
)
|
||||
}
|
||||
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<View>(() => {
|
||||
setVisible(false)
|
||||
if (targetRef.current === ref.current) {
|
||||
onClickOutside()
|
||||
const value = useMemo<PluginTooltipsContextValue>(() => {
|
||||
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 (
|
||||
<ClickOutsideProvider>
|
||||
<PluginTooltipsContext.Provider value={value}>
|
||||
{children}
|
||||
</PluginTooltipsContext.Provider>
|
||||
</ClickOutsideProvider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<View>(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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<LayerScope>
|
||||
<ClickOutsideProvider>
|
||||
<PluginStatesProvider>
|
||||
<Page spacing={16}>
|
||||
<EssentialPluginTooltipProvider>
|
||||
<EnablePluginTooltipProvider>
|
||||
<Screen />
|
||||
</EnablePluginTooltipProvider>
|
||||
</EssentialPluginTooltipProvider>
|
||||
</Page>
|
||||
</PluginStatesProvider>
|
||||
</ClickOutsideProvider>
|
||||
<PluginStatesProvider>
|
||||
<Page spacing={16}>
|
||||
<PluginTooltipsProvider>
|
||||
<Screen />
|
||||
</PluginTooltipsProvider>
|
||||
</Page>
|
||||
</PluginStatesProvider>
|
||||
</LayerScope>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<LayerScope>
|
||||
<ClickOutsideProvider>
|
||||
<PluginStatesProvider>
|
||||
<PluginStatesProvider>
|
||||
<PluginTooltipsProvider>
|
||||
<Page spacing={16}>
|
||||
<EssentialPluginTooltipProvider>
|
||||
<EnablePluginTooltipProvider>
|
||||
<Screen />
|
||||
</EnablePluginTooltipProvider>
|
||||
</EssentialPluginTooltipProvider>
|
||||
<Screen />
|
||||
</Page>
|
||||
</PluginStatesProvider>
|
||||
</ClickOutsideProvider>
|
||||
</PluginTooltipsProvider>
|
||||
</PluginStatesProvider>
|
||||
</LayerScope>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue