refactor(desktop): singleton modal stack present method
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
3e9fd61575
commit
91c8d312ea
|
|
@ -2,6 +2,8 @@ import type { FC, RefObject } from "react"
|
|||
import { createContext as reactCreateContext } from "react"
|
||||
import { createContext as createContextSelector } from "use-context-selector"
|
||||
|
||||
import type { ModalProps } from "./types"
|
||||
|
||||
export type CurrentModalContentProps = ModalActionsInternal & {
|
||||
ref: RefObject<HTMLElement | null>
|
||||
modalElementRef: RefObject<HTMLElement | null>
|
||||
|
|
@ -37,3 +39,10 @@ export type ModalActionsInternal = {
|
|||
setClickOutSideToDismiss: (value: boolean) => void
|
||||
getIndex: () => number
|
||||
}
|
||||
|
||||
type Disposer = () => void
|
||||
type PresentModalContextInternalFn = (props: ModalProps & { id?: string }) => Disposer
|
||||
export const PresentModalContextInternal = reactCreateContext<PresentModalContextInternalFn>(() => {
|
||||
warnNoProvider()
|
||||
return () => {}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,75 +1,30 @@
|
|||
import { Button } from "@follow/components/ui/button/index.js"
|
||||
import { nextFrame } from "@follow/utils/dom"
|
||||
import { atom, useAtomValue } from "jotai"
|
||||
import type { DragControls } from "motion/react"
|
||||
import type { ResizeCallback, ResizeStartCallback } from "re-resizable"
|
||||
import { use, useId, useRef, useState } from "react"
|
||||
import { use, useState } from "react"
|
||||
import { flushSync } from "react-dom"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useContextSelector } from "use-context-selector"
|
||||
import { useEventCallback } from "usehooks-ts"
|
||||
|
||||
import { getUISettings } from "~/atoms/settings/ui"
|
||||
import { jotaiStore } from "~/lib/jotai"
|
||||
|
||||
import { modalStackAtom } from "./atom"
|
||||
import { ModalEventBus } from "./bus"
|
||||
import { CurrentModalContext, CurrentModalStateContext } from "./context"
|
||||
import type { DialogInstance, ModalProps, ModalStackOptions } from "./types"
|
||||
import {
|
||||
CurrentModalContext,
|
||||
CurrentModalStateContext,
|
||||
PresentModalContextInternal,
|
||||
} from "./context"
|
||||
import type { DialogInstance, ModalProps } from "./types"
|
||||
|
||||
export const modalIdToPropsMap = {} as Record<string, ModalProps>
|
||||
export const useModalStack = (options?: ModalStackOptions) => {
|
||||
const id = useId()
|
||||
const currentCount = useRef(0)
|
||||
const { wrapper } = options || {}
|
||||
export const useModalStack = () => {
|
||||
const present = use(PresentModalContextInternal)
|
||||
|
||||
return {
|
||||
present: useEventCallback((props: ModalProps & { id?: string }) => {
|
||||
const presentSync = (props: ModalProps & { id?: string }) => {
|
||||
const fallbackModelId = `${id}-${++currentCount.current}`
|
||||
const modalId = props.id ?? fallbackModelId
|
||||
|
||||
const currentStack = jotaiStore.get(modalStackAtom)
|
||||
|
||||
const existingModal = currentStack.find((item) => item.id === modalId)
|
||||
|
||||
if (existingModal) {
|
||||
// Move to top
|
||||
jotaiStore.set(modalStackAtom, (p) => {
|
||||
const index = p.indexOf(existingModal)
|
||||
return [...p.slice(0, index), ...p.slice(index + 1), existingModal]
|
||||
})
|
||||
} else {
|
||||
// NOTE: The props of the Command Modal are immutable, so we'll just take the store value and inject it.
|
||||
// There is no need to inject `overlay` props, this is rendered responsively based on ui changes.
|
||||
const uiSettings = getUISettings()
|
||||
const modalConfig: Partial<ModalProps> = {
|
||||
draggable: uiSettings.modalDraggable,
|
||||
modal: true,
|
||||
}
|
||||
jotaiStore.set(modalStackAtom, (p) => {
|
||||
const modalProps: ModalProps = {
|
||||
...modalConfig,
|
||||
...props,
|
||||
|
||||
wrapper,
|
||||
}
|
||||
modalIdToPropsMap[modalId] = modalProps
|
||||
return p.concat({
|
||||
id: modalId,
|
||||
...modalProps,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
jotaiStore.set(modalStackAtom, (p) => p.filter((item) => item.id !== modalId))
|
||||
}
|
||||
}
|
||||
|
||||
return nextFrame(() => presentSync(props))
|
||||
}),
|
||||
|
||||
present,
|
||||
...actions,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,73 @@
|
|||
import { nextFrame } from "@follow/utils/dom"
|
||||
import type { FC, PropsWithChildren } from "react"
|
||||
import { useId, useRef } from "react"
|
||||
import { useEventCallback } from "usehooks-ts"
|
||||
|
||||
import { getUISettings } from "~/atoms/settings/ui"
|
||||
import { jotaiStore } from "~/lib/jotai"
|
||||
|
||||
import { modalStackAtom } from "./atom"
|
||||
import { PresentModalContextInternal } from "./context"
|
||||
import { modalIdToPropsMap } from "./hooks"
|
||||
import { ModalStack } from "./modal-stack"
|
||||
import type { ModalProps } from "./types"
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
presentModal: (modal: ModalProps) => void
|
||||
presentModal: (modal: ModalProps & { id?: string }) => void
|
||||
}
|
||||
}
|
||||
|
||||
export const ModalStackProvider: FC<PropsWithChildren> = ({ children }) => (
|
||||
<>
|
||||
{children}
|
||||
<ModalStack />
|
||||
</>
|
||||
)
|
||||
export const ModalStackProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const id = useId()
|
||||
const currentCount = useRef(0)
|
||||
const presentModal = useEventCallback((props: ModalProps & { id?: string }) => {
|
||||
const fallbackModelId = `${id}-${++currentCount.current}`
|
||||
const modalId = props.id ?? fallbackModelId
|
||||
const presentSync = (props: ModalProps & { id?: string }) => {
|
||||
const currentStack = jotaiStore.get(modalStackAtom)
|
||||
|
||||
const existingModal = currentStack.find((item) => item.id === modalId)
|
||||
|
||||
if (existingModal) {
|
||||
// Move to top
|
||||
jotaiStore.set(modalStackAtom, (p) => {
|
||||
const index = p.indexOf(existingModal)
|
||||
return [...p.slice(0, index), ...p.slice(index + 1), existingModal]
|
||||
})
|
||||
} else {
|
||||
// NOTE: The props of the Command Modal are immutable, so we'll just take the store value and inject it.
|
||||
// There is no need to inject `overlay` props, this is rendered responsively based on ui changes.
|
||||
const uiSettings = getUISettings()
|
||||
const modalConfig: Partial<ModalProps> = {
|
||||
draggable: uiSettings.modalDraggable,
|
||||
modal: true,
|
||||
}
|
||||
jotaiStore.set(modalStackAtom, (p) => {
|
||||
const modalProps: ModalProps = {
|
||||
...modalConfig,
|
||||
...props,
|
||||
}
|
||||
modalIdToPropsMap[modalId] = modalProps
|
||||
return p.concat({
|
||||
id: modalId,
|
||||
...modalProps,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
nextFrame(() => presentSync(props))
|
||||
|
||||
return () => {
|
||||
jotaiStore.set(modalStackAtom, (p) => p.filter((item) => item.id !== modalId))
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<PresentModalContextInternal value={presentModal}>
|
||||
{children}
|
||||
<ModalStack />
|
||||
</PresentModalContextInternal>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,9 +32,6 @@ export interface ModalProps {
|
|||
|
||||
autoFocus?: boolean
|
||||
}
|
||||
export interface ModalStackOptions {
|
||||
wrapper?: FC
|
||||
}
|
||||
|
||||
export interface DialogInstance {
|
||||
ask: (options: {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,5 @@ export { ContextMenuProvider as LazyContextMenuProvider } from "../context-menu-
|
|||
export { ExtensionExposeProvider as LazyExtensionExposeProvider } from "../extension-expose-provider"
|
||||
export { ExternalJumpInProvider as LazyExternalJumpInProvider } from "../external-jump-in-provider"
|
||||
export { LottieRenderContainer as LazyLottieRenderContainer } from "~/components/ui/lottie-container"
|
||||
export { ModalStackProvider as LazyModalStackProvider } from "~/components/ui/modal"
|
||||
export const LazyReloadPrompt = () => null
|
||||
export const LazyPWAPrompt = () => null
|
||||
|
|
|
|||
|
|
@ -10,11 +10,6 @@ const LazyContextMenuProvider = lazy(() =>
|
|||
default: res.ContextMenuProvider,
|
||||
})),
|
||||
)
|
||||
const LazyModalStackProvider = lazy(() =>
|
||||
import("../../components/ui/modal/stacked/provider").then((res) => ({
|
||||
default: res.ModalStackProvider,
|
||||
})),
|
||||
)
|
||||
|
||||
const LazyExtensionExposeProvider = lazy(() =>
|
||||
import("./../extension-expose-provider").then((res) => ({
|
||||
|
|
@ -52,7 +47,6 @@ export {
|
|||
LazyContextMenuProvider,
|
||||
LazyExtensionExposeProvider,
|
||||
LazyLottieRenderContainer,
|
||||
LazyModalStackProvider,
|
||||
LazyPWAPrompt,
|
||||
LazyReloadPrompt,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { Suspense } from "react"
|
|||
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3"
|
||||
import { HotkeysProvider } from "react-hotkeys-hook"
|
||||
|
||||
import { ModalStackProvider } from "~/components/ui/modal"
|
||||
import { HotKeyScopeMap } from "~/constants/hotkeys"
|
||||
import { jotaiStore } from "~/lib/jotai"
|
||||
import { persistConfig, queryClient } from "~/lib/query-client"
|
||||
|
|
@ -24,7 +25,6 @@ import {
|
|||
LazyExtensionExposeProvider,
|
||||
LazyExternalJumpInProvider,
|
||||
LazyLottieRenderContainer,
|
||||
LazyModalStackProvider,
|
||||
LazyPWAPrompt,
|
||||
LazyReloadPrompt,
|
||||
// specific import should add `index` postfix
|
||||
|
|
@ -39,40 +39,41 @@ export const RootProviders: FC<PropsWithChildren> = ({ children }) => (
|
|||
useEnterprise={true}
|
||||
useRecaptchaNet={true}
|
||||
>
|
||||
<MotionProvider>
|
||||
<PersistQueryClientProvider persistOptions={persistConfig} client={queryClient}>
|
||||
<HotkeysProvider initiallyActiveScopes={HotKeyScopeMap.Home}>
|
||||
<Provider store={jotaiStore}>
|
||||
<Provider store={jotaiStore}>
|
||||
<MotionProvider>
|
||||
<PersistQueryClientProvider persistOptions={persistConfig} client={queryClient}>
|
||||
<HotkeysProvider initiallyActiveScopes={HotKeyScopeMap.Home}>
|
||||
<I18nProvider>
|
||||
<Toaster />
|
||||
<EventProvider />
|
||||
<ModalStackProvider>
|
||||
<Toaster />
|
||||
<EventProvider />
|
||||
|
||||
<UserProvider />
|
||||
<ServerConfigsProvider />
|
||||
<UserProvider />
|
||||
<ServerConfigsProvider />
|
||||
|
||||
<StableRouterProvider />
|
||||
<SettingSync />
|
||||
<FollowCommandManager />
|
||||
{import.meta.env.DEV && <Devtools />}
|
||||
<StableRouterProvider />
|
||||
<SettingSync />
|
||||
<FollowCommandManager />
|
||||
{import.meta.env.DEV && <Devtools />}
|
||||
|
||||
{children}
|
||||
{children}
|
||||
|
||||
<Suspense>
|
||||
<LazyExtensionExposeProvider />
|
||||
<LazyModalStackProvider />
|
||||
<LazyContextMenuProvider />
|
||||
<LazyLottieRenderContainer />
|
||||
<LazyExternalJumpInProvider />
|
||||
<LazyReloadPrompt />
|
||||
{!window.__RN__ && <LazyPWAPrompt />}
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<LazyExtensionExposeProvider />
|
||||
<LazyContextMenuProvider />
|
||||
<LazyLottieRenderContainer />
|
||||
<LazyExternalJumpInProvider />
|
||||
<LazyReloadPrompt />
|
||||
{!window.__RN__ && <LazyPWAPrompt />}
|
||||
</Suspense>
|
||||
</ModalStackProvider>
|
||||
</I18nProvider>
|
||||
</Provider>
|
||||
</HotkeysProvider>
|
||||
</HotkeysProvider>
|
||||
|
||||
<InvalidateQueryProvider />
|
||||
</PersistQueryClientProvider>
|
||||
</MotionProvider>
|
||||
<InvalidateQueryProvider />
|
||||
</PersistQueryClientProvider>
|
||||
</MotionProvider>
|
||||
</Provider>
|
||||
</GoogleReCaptchaProvider>
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue