diff --git a/packages/app/package.json b/packages/app/package.json index 4771ed5a0..236d8ca3b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -37,7 +37,8 @@ "quill-cursors": "^4.0.0", "react": "18.2.0", "react-dom": "18.2.0", - "yjs": "^13.5.45" + "yjs": "^13.5.45", + "zustand": "^4.3.2" }, "devDependencies": { "@types/node": "18.7.18", diff --git a/packages/app/src/components/header/QuickSearchButton.tsx b/packages/app/src/components/header/QuickSearchButton.tsx index 9b73c096f..d32be2c03 100644 --- a/packages/app/src/components/header/QuickSearchButton.tsx +++ b/packages/app/src/components/header/QuickSearchButton.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { IconButton, IconButtonProps } from '@affine/component'; import { ArrowDownIcon } from '@blocksuite/icons'; -import { useModal } from '@/providers/GlobalModalProvider'; +import { useModal } from '@/store/globalModal'; import { styled } from '@affine/component'; const StyledIconButtonWithAnimate = styled(IconButton)(({ theme }) => { diff --git a/packages/app/src/components/header/header-right-items/SyncUser.tsx b/packages/app/src/components/header/header-right-items/SyncUser.tsx index 0cedea6fe..8724f6b42 100644 --- a/packages/app/src/components/header/header-right-items/SyncUser.tsx +++ b/packages/app/src/components/header/header-right-items/SyncUser.tsx @@ -1,5 +1,5 @@ import { CloudUnsyncedIcon } from '@blocksuite/icons'; -import { useModal } from '@/providers/GlobalModalProvider'; +import { useModal } from '@/store/globalModal'; import { useAppState } from '@/providers/app-state-provider'; import { IconButton } from '@affine/component'; diff --git a/packages/app/src/components/help-island/index.tsx b/packages/app/src/components/help-island/index.tsx index 714e43d04..0f01f2713 100644 --- a/packages/app/src/components/help-island/index.tsx +++ b/packages/app/src/components/help-island/index.tsx @@ -9,7 +9,7 @@ import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons'; import { Tooltip } from '@affine/component'; import { useTranslation } from '@affine/i18n'; -import { useModal } from '@/providers/GlobalModalProvider'; +import { useModal } from '@/store/globalModal'; import { MuiFade } from '@affine/component'; export type IslandItemNames = 'contact' | 'shortcuts'; export const HelpIsland = ({ diff --git a/packages/app/src/components/quick-search/index.tsx b/packages/app/src/components/quick-search/index.tsx index 3ca942444..5df443c0a 100644 --- a/packages/app/src/components/quick-search/index.tsx +++ b/packages/app/src/components/quick-search/index.tsx @@ -11,7 +11,7 @@ import { Results } from './Results'; import { Footer } from './Footer'; import { Command } from 'cmdk'; import { useEffect, useState } from 'react'; -import { useModal } from '@/providers/GlobalModalProvider'; +import { useModal } from '@/store/globalModal'; import { getUaHelper } from '@/utils'; import { useRouter } from 'next/router'; import { PublishedResults } from './PublishedResults'; diff --git a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/LoginItem.tsx b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/LoginItem.tsx index 7ea99ecce..a000de38f 100644 --- a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/LoginItem.tsx +++ b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/LoginItem.tsx @@ -1,4 +1,4 @@ -import { useModal } from '@/providers/GlobalModalProvider'; +import { useModal } from '@/store/globalModal'; import { styled } from '@affine/component'; import { AffineIcon } from '../../icons/Icons'; import { diff --git a/packages/app/src/components/workspace-slider-bar/index.tsx b/packages/app/src/components/workspace-slider-bar/index.tsx index 5c98f28ff..7507569fd 100644 --- a/packages/app/src/components/workspace-slider-bar/index.tsx +++ b/packages/app/src/components/workspace-slider-bar/index.tsx @@ -23,7 +23,7 @@ import { import Link from 'next/link'; import { MuiCollapse } from '@affine/component'; import { Tooltip } from '@affine/component'; -import { useModal } from '@/providers/GlobalModalProvider'; +import { useModal } from '@/store/globalModal'; import { useAppState } from '@/providers/app-state-provider'; import { IconButton } from '@affine/component'; import useLocalStorage from '@/hooks/use-local-storage'; diff --git a/packages/app/src/pages/_app.tsx b/packages/app/src/pages/_app.tsx index f572774a0..562d3657a 100644 --- a/packages/app/src/pages/_app.tsx +++ b/packages/app/src/pages/_app.tsx @@ -12,7 +12,7 @@ import type { PropsWithChildren, ReactElement, ReactNode } from 'react'; import type { NextPage } from 'next'; import { AppStateProvider } from '@/providers/app-state-provider'; import ConfirmProvider from '@/providers/ConfirmProvider'; -import { ModalProvider } from '@/providers/GlobalModalProvider'; +import { ModalProvider } from '@/store/globalModal'; // import AppStateProvider2 from '@/providers/app-state-provider2/provider'; import { useRouter } from 'next/router'; diff --git a/packages/app/src/pages/public-workspace/[workspaceId]/[pageId].tsx b/packages/app/src/pages/public-workspace/[workspaceId]/[pageId].tsx index 4ccda04b7..841174068 100644 --- a/packages/app/src/pages/public-workspace/[workspaceId]/[pageId].tsx +++ b/packages/app/src/pages/public-workspace/[workspaceId]/[pageId].tsx @@ -11,7 +11,7 @@ import { IconButton } from '@affine/component'; import NextLink from 'next/link'; import { PaperIcon, SearchIcon } from '@blocksuite/icons'; import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; -import { useModal } from '@/providers/GlobalModalProvider'; +import { useModal } from '@/store/globalModal'; const DynamicBlocksuite = dynamic(() => import('@/components/editor'), { ssr: false, diff --git a/packages/app/src/pages/public-workspace/[workspaceId]/index.tsx b/packages/app/src/pages/public-workspace/[workspaceId]/index.tsx index 37aaa7317..4640d595f 100644 --- a/packages/app/src/pages/public-workspace/[workspaceId]/index.tsx +++ b/packages/app/src/pages/public-workspace/[workspaceId]/index.tsx @@ -11,7 +11,7 @@ import { import { Breadcrumbs } from '@affine/component'; import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; import { SearchIcon } from '@blocksuite/icons'; -import { useModal } from '@/providers/GlobalModalProvider'; +import { useModal } from '@/store/globalModal'; const All = () => { const { dataCenter } = useAppState(); const router = useRouter(); diff --git a/packages/app/src/providers/GlobalModalProvider.tsx b/packages/app/src/providers/GlobalModalProvider.tsx deleted file mode 100644 index 76a0365df..000000000 --- a/packages/app/src/providers/GlobalModalProvider.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ -import { - createContext, - useCallback, - useContext, - useEffect, - useState, -} from 'react'; -import type { PropsWithChildren } from 'react'; -import ShortcutsModal from '@/components/shortcuts-modal'; -import ContactModal from '@/components/contact-modal'; -import QuickSearch from '@/components/quick-search'; -import { ImportModal } from '@/components/import'; -import { LoginModal } from '@/components/login-modal'; - -type ModalContextValue = { - triggerShortcutsModal: () => void; - triggerContactModal: () => void; - triggerQuickSearchModal: (visible?: boolean) => void; - triggerImportModal: () => void; - triggerLoginModal: () => void; -}; -type ModalContextProps = PropsWithChildren>; -type ModalMap = { - contact: boolean; - shortcuts: boolean; - quickSearch: boolean; - import: boolean; - login: boolean; -}; - -export const ModalContext = createContext({ - triggerShortcutsModal: () => {}, - triggerContactModal: () => {}, - triggerQuickSearchModal: () => {}, - triggerImportModal: () => {}, - triggerLoginModal: () => {}, -}); - -export const useModal = () => useContext(ModalContext); - -export const ModalProvider = ({ - children, -}: PropsWithChildren) => { - const [modalMap, setModalMap] = useState({ - contact: false, - shortcuts: false, - quickSearch: false, - import: false, - login: false, - }); - - const triggerHandler = useCallback( - (key: keyof ModalMap, visible?: boolean) => { - setModalMap({ - ...modalMap, - [key]: visible ?? !modalMap[key], - }); - }, - [modalMap] - ); - useEffect(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - window.triggerHandler = () => triggerHandler('login'); - }, [triggerHandler]); - - return ( - { - triggerHandler('shortcuts'); - }, - triggerContactModal: () => { - triggerHandler('contact'); - }, - triggerQuickSearchModal: (visible?) => { - triggerHandler('quickSearch', visible); - }, - triggerImportModal: () => { - triggerHandler('import'); - }, - triggerLoginModal: () => { - triggerHandler('login'); - }, - }} - > - { - triggerHandler('contact', false); - }} - > - { - triggerHandler('shortcuts', false); - }} - > - { - triggerHandler('quickSearch', false); - }} - > - { - triggerHandler('import', false); - }} - > - { - triggerHandler('login', false); - }} - /> - {children} - - ); -}; - -export default ModalProvider; diff --git a/packages/app/src/store/globalModal/index.tsx b/packages/app/src/store/globalModal/index.tsx new file mode 100644 index 000000000..337a8fff6 --- /dev/null +++ b/packages/app/src/store/globalModal/index.tsx @@ -0,0 +1,146 @@ +import type React from 'react'; +import { createContext, useCallback, useContext, useMemo } from 'react'; +import { createStore, useStore } from 'zustand'; +import { combine, subscribeWithSelector } from 'zustand/middleware'; +import { UseBoundStore } from 'zustand/react'; +import ContactModal from '@/components/contact-modal'; +import ShortcutsModal from '@/components/shortcuts-modal'; +import QuickSearch from '@/components/quick-search'; +import { LoginModal } from '@/components/login-modal'; +import ImportModal from '@/components/import'; + +export type ModalState = { + contact: boolean; + shortcuts: boolean; + quickSearch: boolean; + import: boolean; + login: boolean; +}; + +export type ModalActions = { + triggerShortcutsModal: () => void; + triggerContactModal: () => void; + triggerQuickSearchModal: (visible?: boolean) => void; + triggerImportModal: () => void; + triggerLoginModal: () => void; +}; + +const create = () => + createStore( + subscribeWithSelector( + combine( + { + contact: false, + shortcuts: false, + quickSearch: false, + import: false, + login: false, + }, + set => ({ + triggerShortcutsModal: () => { + set(({ shortcuts }) => ({ + shortcuts: !shortcuts, + })); + }, + triggerContactModal: () => { + set(({ contact }) => ({ + contact: !contact, + })); + }, + triggerQuickSearchModal: (visible?: boolean) => { + set(({ quickSearch }) => ({ + quickSearch: visible ?? !quickSearch, + })); + }, + triggerImportModal: () => { + set(state => ({ + import: !state.import, + })); + }, + triggerLoginModal: () => { + set(({ login }) => ({ + login: !login, + })); + }, + }) + ) + ) + ); +type Store = ReturnType; + +const ModalContext = createContext(null); + +export const useModalApi = () => { + const api = useContext(ModalContext); + if (!api) { + throw new Error('cannot find modal context'); + } + return api; +}; + +export const useModal: UseBoundStore = (( + selector: Parameters>[0], + equals: Parameters>[1] +) => { + const api = useModalApi(); + return useStore(api, selector, equals); + // eslint-disable-next-line @typescript-eslint/no-explicit-any +}) as any; + +const Modals: React.FC = function Modal() { + const api = useModalApi(); + return ( + <> + state.contact)} + onClose={useCallback(() => { + api.setState({ + contact: false, + }); + }, [api])} + > + state.shortcuts)} + onClose={useCallback(() => { + api.setState({ + shortcuts: false, + }); + }, [api])} + > + state.quickSearch)} + onClose={useCallback(() => { + api.setState({ + quickSearch: false, + }); + }, [api])} + > + state.import)} + onClose={useCallback(() => { + api.setState({ + import: false, + }); + }, [api])} + > + state.login)} + onClose={useCallback(() => { + api.setState({ + login: false, + }); + }, [api])} + /> + + ); +}; + +export const ModalProvider: React.FC = + function ModelProvider({ children }) { + return ( + create(), [])}> + + {children} + + ); + }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ded45584..5026a0261 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,7 @@ importers: react-dom: 18.2.0 typescript: ^4.9.5 yjs: ^13.5.45 + zustand: ^4.3.2 dependencies: '@affine/component': link:../component '@affine/datacenter': link:../data-center @@ -120,6 +121,7 @@ importers: react: 18.2.0 react-dom: 18.2.0_react@18.2.0 yjs: 13.5.45 + zustand: 4.3.2_react@18.2.0 devDependencies: '@types/node': 18.7.18 '@types/react': 18.0.20 @@ -18152,6 +18154,22 @@ packages: resolution: {integrity: sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==} dev: false + /zustand/4.3.2_react@18.2.0: + resolution: {integrity: sha512-rd4haDmlwMTVWVqwvgy00ny8rtti/klRoZjFbL/MAcDnmD5qSw/RZc+Vddstdv90M5Lv6RPgWvm1Hivyn0QgJw==} + engines: {node: '>=12.7.0'} + peerDependencies: + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + immer: + optional: true + react: + optional: true + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0_react@18.2.0 + dev: false + /zwitch/1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} dev: true