diff --git a/packages/desktop/package.json b/packages/desktop/package.json index f8dc8ec..68aa981 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -93,6 +93,7 @@ "typescript": "~4.5.4" }, "dependencies": { + "@100mslive/react-sdk": "^0.1.0", "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@getstation/electron-google-oauth2": "^2.1.0", diff --git a/packages/desktop/src/app.tsx b/packages/desktop/src/app.tsx index 670cf93..d802760 100644 --- a/packages/desktop/src/app.tsx +++ b/packages/desktop/src/app.tsx @@ -6,6 +6,7 @@ import { ElectronProvider } from './providers/ElectronProvider'; import ErrorParent from './providers/ErrorBoundary'; import { NirvanaTheme } from './mui/NirvanaTheme'; import ReactDOM from 'react-dom/client'; +import { SearchProvider } from './providers/SearchProvider'; import { SnackbarProvider } from 'notistack'; import { TerminalProvider } from './components/Terminal'; import { ThemeProvider } from '@mui/material'; @@ -29,9 +30,11 @@ root.render( - - <> - + + + <> + + diff --git a/packages/desktop/src/components/Navbar.tsx b/packages/desktop/src/components/Navbar.tsx index 51c08f0..f9aab4f 100644 --- a/packages/desktop/src/components/Navbar.tsx +++ b/packages/desktop/src/components/Navbar.tsx @@ -1,41 +1,23 @@ -import { - Avatar, - CircularProgress, - Divider, - IconButton, - Input, - ListItemIcon, - Menu, - Stack, - Tooltip, - Typography, -} from '@mui/material'; -import { FiHeadphones, FiLogOut, FiMonitor, FiSearch, FiUsers, FiWind } from 'react-icons/fi'; +import { CircularProgress, IconButton, Input, Stack, Tooltip } from '@mui/material'; +import { FiSearch, FiUsers } from 'react-icons/fi'; import React, { useCallback, useRef } from 'react'; import { useKeyPressEvent, useRendersCount } from 'react-use'; import KeyboardShortcutLabel from './KeyboardShortcutLabel'; import { KeyboardShortcuts } from '../util/keyboard'; -import MenuItem from '@mui/material/MenuItem'; import NirvanaLogo from './NirvanaLogo'; import { blueGrey } from '@mui/material/colors'; -import useAuth from '../providers/AuthProvider'; +import useSearch from '../providers/SearchProvider'; import { useSnackbar } from 'notistack'; import useTerminal from './Terminal'; -const Navbar = ({ - handleChangeSearchInput, - searchVal, - isSearching, -}: { - handleChangeSearchInput: React.ChangeEventHandler; - searchVal: string; - isSearching: boolean; -}) => { +const Navbar = () => { const { enqueueSnackbar } = useSnackbar(); const { handleShowCreateConvoForm } = useTerminal(); + const { searchQuery, omniSearch, conversationResults, userResults, isSearching } = useSearch(); + const searchRef = useRef(null); const onSearchFocus = useCallback(() => { if (searchRef?.current) searchRef.current.focus(); @@ -46,6 +28,13 @@ const Navbar = ({ const rendersCount = useRendersCount(); console.warn('RENDER COUNT | NAVBAR | ', rendersCount); + const handleSearchChange = useCallback( + (e: React.ChangeEvent) => { + omniSearch(e.target.value); + }, + [omniSearch], + ); + return ( {isSearching && } - {searchVal ? ( + {searchQuery ? ( ) : ( diff --git a/packages/desktop/src/components/OmniSearchResults.tsx b/packages/desktop/src/components/OmniSearchResults.tsx index 617401b..93d5727 100644 --- a/packages/desktop/src/components/OmniSearchResults.tsx +++ b/packages/desktop/src/components/OmniSearchResults.tsx @@ -17,18 +17,15 @@ import { FiZap } from 'react-icons/fi'; import React from 'react'; import { User } from '@nirvana/core/src/models/user.model'; import useConversations from '../providers/ConversationProvider'; +import useSearch from '../providers/SearchProvider'; import useTerminal from './Terminal'; -export default function OmniSearchResults({ - people, - conversations, -}: { - people: User[]; - conversations: Conversation[]; -}) { +export default function OmniSearchResults() { const { handleQuickDial } = useTerminal(); const { selectConversation } = useConversations(); + const { userResults: people, conversationResults: conversations } = useSearch(); + return ( <> void; @@ -70,13 +58,13 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { const { conversationMap, conversationContentMap, selectConversation, selectedConversation } = useConversations(); - const { searchRelevantConversations } = useSearchConversations(conversationMap); + const { clearSearch, searchQuery } = useSearch(); // handle create or open existing conversation // not 100% consistent to the second, but still works...don't need atomicity const handleQuickDial = useCallback( async (otherUser: User) => { - setSearchVal(''); + clearSearch(); try { // check all current conversations which should be live listened to @@ -105,7 +93,7 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { enqueueSnackbar('Something went wrong, please try again', { variant: 'error' }); } }, - [conversationMap, enqueueSnackbar, nirvanaUser, selectConversation], + [conversationMap, enqueueSnackbar, nirvanaUser, selectConversation, clearSearch], ); const [isUserSpeaking, setIsUserSpeaking] = useState(false); @@ -235,54 +223,6 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { useKeyPressEvent('`', handleBroadcast, handleStopBroadcast); - const [searchVal, setSearchVal] = useState(''); - const [searchUsersResults, setSearchUsersResults] = useState([]); - const [searchConversationsResults, setSearchConversationsResults] = useState([]); - const [searching, setSearching] = useState(false); - - // debounce user search - const [, cancel] = useDebounce( - async () => { - if (searchVal) { - let results = await searchUsers(searchVal); - results = results.filter((userResult) => userResult.id !== user.uid); - setSearchUsersResults(results); - - console.warn('searched users', results); - } - - setSearching(false); - }, - 1000, - [searchVal, setSearchUsersResults, user], - ); - - const handleOmniSearch = useCallback( - async (searchQuery: string) => { - searchQuery = searchQuery.replace('/', ''); - setSearchVal(searchQuery); - - if (!searchQuery) { - return; - } - - // user search - setSearching(true); - - // client side conversation search - const relevantConversations = await searchRelevantConversations(searchQuery); - setSearchConversationsResults(relevantConversations); - }, - [setSearching, setSearchVal, setSearchConversationsResults, searchRelevantConversations], - ); - - const handleChangeSearchInput = useCallback( - (e: React.ChangeEvent) => { - handleOmniSearch(e.target.value); - }, - [handleOmniSearch], - ); - const [createConversationMode, setCreateConversationMode] = useState(false); const handleStartConversation = useCallback( @@ -329,8 +269,8 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { const handleEscape = useCallback(() => { setCreateConversationMode(false); selectConversation(undefined); - setSearchVal(''); - }, [selectConversation, setCreateConversationMode, setSearchVal]); + clearSearch(); + }, [selectConversation, setCreateConversationMode, clearSearch]); useKeyPressEvent(KeyboardShortcuts.escape.shortcutKey, handleEscape); @@ -343,7 +283,6 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { createConversationMode, handleShowCreateConvoForm, handleEscape, - handleOmniSearch, }} > @@ -362,22 +301,9 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { flexDirection: 'column', }} > - + - - {searchVal ? ( - - ) : ( - - )} - + {searchQuery ? : } diff --git a/packages/desktop/src/providers/SearchProvider.tsx b/packages/desktop/src/providers/SearchProvider.tsx index e69de29..dc1e0cd 100644 --- a/packages/desktop/src/providers/SearchProvider.tsx +++ b/packages/desktop/src/providers/SearchProvider.tsx @@ -0,0 +1,147 @@ +import React, { useCallback, useContext, useState } from 'react'; + +import Conversation from '@nirvana/core/src/models/conversation.model'; +import { ConversationMap } from '../util/types'; +import { User } from '@nirvana/core/src/models/user.model'; +import { searchUsers as firestoreSearchUsers } from '../firebase/firestore'; +import useAuth from './AuthProvider'; +import useConversations from './ConversationProvider'; +import { useDebounce } from 'react-use'; + +interface ISearchContext { + searchUsers?: (searchQuery: string) => void; + omniSearch?: (searchQuery: string) => void; + searchConversations?: (searchQuery: string) => void; + + conversationResults: Conversation[]; + userResults: User[]; + + isSearching: boolean; + + // transparency into what our search is searching for + searchQuery: string; + clearSearch?: () => void; +} + +const SearchContext = React.createContext({ + isSearching: false, + conversationResults: [], + userResults: [], + searchQuery: '', +}); + +export function SearchProvider({ children }: { children: React.ReactNode }) { + const { user } = useAuth(); + const { conversationMap } = useConversations(); + + const [searchQuery, setSearchQuery] = useState(''); + + const [userResults, setUserResultsResults] = useState([]); + const [conversationResults, setConversationResultsResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + + // debounce user search and any other async ones + const [, cancel] = useDebounce( + async () => { + if (searchQuery) { + let results = await firestoreSearchUsers(searchQuery); + results = results.filter((userResult) => userResult.id !== user.uid); + setUserResultsResults(results); + + console.warn('searched users', results); + } + + setIsSearching(false); + }, + 1000, + [searchQuery, setUserResultsResults, user, setIsSearching], + ); + + // any view will hit this when they want and start the debounce process + const searchUsers = useCallback( + (searchQuery: string) => { + setSearchQuery(searchQuery); + setIsSearching(true); + }, + [setSearchQuery, setIsSearching], + ); + + /** + * + * @param searchQuery + * given a search query, finds conversations + */ + + const searchRelevantConversations = useCallback( + async (searchQuery: string): Promise => { + const relevantConversations: Conversation[] = []; + + Object.values(conversationMap).forEach((conversation) => { + for (const cachedConversationUser of conversation.userCache) { + if ( + cachedConversationUser.email + .toLocaleLowerCase() + .includes(searchQuery.toLocaleLowerCase()) + ) { + relevantConversations.push(conversation); + return; + } + + if ( + cachedConversationUser.displayName + .toLocaleLowerCase() + .includes(searchQuery.toLocaleLowerCase()) + ) { + relevantConversations.push(conversation); + return; + } + } + + if (conversation.name?.toLocaleLowerCase().includes(searchQuery.toLocaleLowerCase())) { + relevantConversations.push(conversation); + return; + } + }); + + return relevantConversations; + }, + [conversationMap], + ); + + const omniSearch = useCallback( + async (searchQuery: string) => { + searchQuery = searchQuery.replace('/', ''); + setSearchQuery(searchQuery); + + // user search + setIsSearching(true); + + // client side conversation search + const relevantConversations = await searchRelevantConversations(searchQuery); + setConversationResultsResults(relevantConversations); + }, + [setIsSearching, setSearchQuery, setConversationResultsResults, searchRelevantConversations], + ); + + const clearSearch = useCallback(() => setSearchQuery(''), [setSearchQuery]); + + return ( + + {children} + + ); +} + +export default function useSearch() { + return useContext(SearchContext); +} diff --git a/packages/desktop/src/util/clientSearch.ts b/packages/desktop/src/util/clientSearch.ts deleted file mode 100644 index 2418680..0000000 --- a/packages/desktop/src/util/clientSearch.ts +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useCallback } from 'react'; - -import Conversation from '@nirvana/core/src/models/conversation.model'; -import { ConversationMap } from './types'; - -/** - * - * @param searchQuery - * given a search query, finds conversations - */ - -export function useSearchConversations(conversationMap: ConversationMap) { - const searchRelevantConversations = useCallback( - async (searchQuery: string): Promise => { - const relevantConversations: Conversation[] = []; - - Object.values(conversationMap).forEach((conversation) => { - for (const cachedConversationUser of conversation.userCache) { - if ( - cachedConversationUser.email - .toLocaleLowerCase() - .includes(searchQuery.toLocaleLowerCase()) - ) { - relevantConversations.push(conversation); - return; - } - - if ( - cachedConversationUser.displayName - .toLocaleLowerCase() - .includes(searchQuery.toLocaleLowerCase()) - ) { - relevantConversations.push(conversation); - return; - } - } - - if (conversation.name?.toLocaleLowerCase().includes(searchQuery.toLocaleLowerCase())) { - relevantConversations.push(conversation); - return; - } - }); - - return relevantConversations; - }, - [conversationMap], - ); - - return { searchRelevantConversations }; -} diff --git a/yarn.lock b/yarn.lock index ce4de56..de04a97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,38 @@ # yarn lockfile v1 +"@100mslive/hms-video-store@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@100mslive/hms-video-store/-/hms-video-store-0.3.0.tgz#8f586d5b318d690ebdd99ea20ed380a944470e5d" + integrity sha512-c14K0/QqVVe46afSR0oKpDCSw6PXNdyCaeqmi5IhQuCAO7LuqBOX0gnP9XlPUjHZJYvXKLIDGRn+scAzje8B0A== + dependencies: + "@100mslive/hms-video" "0.2.0" + eventemitter2 "^6.4.5" + immer "^9.0.6" + reselect "4.0.0" + zustand "3.5.7" + +"@100mslive/hms-video@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@100mslive/hms-video/-/hms-video-0.2.0.tgz#defbb00c0a766adf9a1a61eb1ac64be11ef77dc9" + integrity sha512-YlDuquSY5qh6Lff/ff3hWPe4VtByCXoNH1sGberlAZWOkj6cynHuOYfzzmFH0xtdg/JGxrdL/TdLZoGe1kdSRQ== + dependencies: + eventemitter2 "^6.4.5" + sdp-transform "^2.14.1" + ua-parser-js "^1.0.1" + uuid "^8.3.2" + webrtc-adapter "^8.0.0" + +"@100mslive/react-sdk@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@100mslive/react-sdk/-/react-sdk-0.1.0.tgz#ffef682bb689430465529b1ee2095e8fede6b791" + integrity sha512-zBTdgSvvdhfd3RCT0SnIUrUSWbS3fTNvMqMxZEUxqhlvPAwTzL8+IBXZYmv1gxszdyNFxze8Fa0/vnE05RX1mw== + dependencies: + "@100mslive/hms-video-store" "0.3.0" + react-intersection-observer "^8.33.1" + react-resize-detector "^7.0.0" + zustand "^3.6.2" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" @@ -3414,6 +3446,11 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter2@^6.4.5: + version "6.4.5" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.5.tgz#97380f758ae24ac15df8353e0cc27f8b95644655" + integrity sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -4473,7 +4510,7 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= -immer@^9.0.14: +immer@^9.0.14, immer@^9.0.6: version "9.0.14" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.14.tgz#e05b83b63999d26382bb71676c9d827831248a48" integrity sha512-ubBeqQutOSLIFCUBN03jGeOS6a3DoYlSYwYJTa+gSKEZKU5redJIqkIdZ3JVv/4RZpfcXdAWH5zCNLWPRv2WDw== @@ -6295,6 +6332,11 @@ react-icons@^4.3.1: resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca" integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ== +react-intersection-observer@^8.33.1: + version "8.34.0" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.34.0.tgz#6f6e67831c52e6233f6b6cc7eb55814820137c42" + integrity sha512-TYKh52Zc0Uptp5/b4N91XydfSGKubEhgZRtcg1rhTKABXijc4Sdr1uTp5lJ8TN27jwUsdXxjHXtHa0kPj704sw== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -6305,6 +6347,13 @@ react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-resize-detector@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-7.1.1.tgz#18d5b84909d5ab13abe0a68ddf0fb8e80c553dfc" + integrity sha512-rU54VTstNzFLZAmMNHqt8xINjDWP7SQR05A2HUW0OGvl4vcrXzgaxrrqAY5tZMfkLkoYm5u0i0qGqCjdc2jyAA== + dependencies: + lodash "^4.17.21" + react-transition-group@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" @@ -6484,6 +6533,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -6682,6 +6736,16 @@ screenfull@^5.1.0: resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba" integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== +sdp-transform@^2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.14.1.tgz#2bb443583d478dee217df4caa284c46b870d5827" + integrity sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw== + +sdp@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sdp/-/sdp-3.0.3.tgz#669958d54663ea9f4a46cc66518c9d980c52c61e" + integrity sha512-8EkfckS+XZQaPLyChu4ey7PghrdcraCVNpJe2Gfdi2ON1ylQ7OasuKX+b37R9slnRChwIAiQgt+oj8xXGD8x+A== + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -7500,6 +7564,11 @@ typescript@~4.5.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== +ua-parser-js@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775" + integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -7738,6 +7807,13 @@ webpack@^5.37.0: watchpack "^2.3.1" webpack-sources "^3.2.3" +webrtc-adapter@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-8.1.1.tgz#e4a4dfd1b5085d119da40c4efc0147f7d0961cba" + integrity sha512-1yXevP7TeZGmklEXkvQVrZp3fOSJlLeXNGCA7NovQokxgP3/e2T3EVGL0eKU87S9vKppWjvRWqnJeSANEspOBg== + dependencies: + sdp "^3.0.2" + websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" @@ -7987,3 +8063,13 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@3.5.7: + version "3.5.7" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.5.7.tgz#add5e8d0ba031ce6e0ddf9cb76ef15306efb665f" + integrity sha512-DlVFXJavIHyXTOGz6dB+8QHZsPyJcGJSEBtlp2Ivmd5SwtlCnhPo3L8LB6YRfAOJC2PbqzgoD8NMjk+y+vIF0g== + +zustand@^3.6.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d" + integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==