From 57351d31c9f6b8c68f9697c5f4b6c99a9e985c10 Mon Sep 17 00:00:00 2001 From: talksik Date: Sat, 11 Jun 2022 06:35:23 -0500 Subject: [PATCH] adding in search and router --- .vscode/settings.json | 2 +- .../desktop/src/providers/RouterProvider.tsx | 30 +++ .../desktop/src/providers/SearchProvider.tsx | 147 +++++++++++++ packages/desktop/src/tree/Navbar.tsx | 9 +- .../src/tree/NewConversationDialog.tsx | 193 ++++++++++++++++++ packages/desktop/src/tree/Terminal.tsx | 8 + packages/desktop/src/tree/electronApp.tsx | 8 +- 7 files changed, 391 insertions(+), 6 deletions(-) create mode 100644 packages/desktop/src/providers/RouterProvider.tsx create mode 100644 packages/desktop/src/providers/SearchProvider.tsx create mode 100644 packages/desktop/src/tree/NewConversationDialog.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 2455858..abfc7e4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,7 @@ "editor.tabSize": 2, "workbench.iconTheme": "material-icon-theme", "editor.defaultFormatter": "esbenp.prettier-vscode", - "workbench.colorTheme": "Quiet Light", + "workbench.colorTheme": "Default Light+", "javascript.format.semicolons": "insert", "window.zoomLevel": 1 diff --git a/packages/desktop/src/providers/RouterProvider.tsx b/packages/desktop/src/providers/RouterProvider.tsx new file mode 100644 index 0000000..47b5ab0 --- /dev/null +++ b/packages/desktop/src/providers/RouterProvider.tsx @@ -0,0 +1,30 @@ +// make do router given absense of react router in desktop apps + +import React, { useState } from 'react'; + +export enum EPage { + START_NEW_CONVERSATION = 'START_NEW_CONVERSATION', + FULL_PAGE_OMNI_SEARCH = 'FULL_PAGE_OMNI_SEARCH', + CONVERSATION_DETAILS = 'CONVERSATION_DETAILS', + PROFILE = 'PROFILE', + WELCOME = 'WELCOME', +} + +interface IRouterContext { + page: EPage; + setPage?: React.Dispatch>; +} + +const RouterContext = React.createContext({ + page: EPage.START_NEW_CONVERSATION, +}); + +export function RouterProvider({ children }: { children: React.ReactNode }) { + const [page, setPage] = useState(EPage.START_NEW_CONVERSATION); + + return {children}; +} + +export default function useRouter() { + return React.useContext(RouterContext); +} diff --git a/packages/desktop/src/providers/SearchProvider.tsx b/packages/desktop/src/providers/SearchProvider.tsx new file mode 100644 index 0000000..f639036 --- /dev/null +++ b/packages/desktop/src/providers/SearchProvider.tsx @@ -0,0 +1,147 @@ +import React, { useCallback, useContext, useState } from 'react'; + +import Conversation from '@nirvana/core/models/conversation.model'; +import { ConversationMap } from '../util/types'; +import User from '@nirvana/core/models/user.model'; +import useAuth from './AuthProvider'; +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. user._id); + + const results = []; + 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; + }, + [], + ); + + 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/tree/Navbar.tsx b/packages/desktop/src/tree/Navbar.tsx index 150e603..a1d0702 100644 --- a/packages/desktop/src/tree/Navbar.tsx +++ b/packages/desktop/src/tree/Navbar.tsx @@ -7,10 +7,11 @@ import KeyboardShortcutLabel from '../subcomponents/KeyboardShortcutLabel'; import { KeyboardShortcuts } from '../util/keyboard'; import NirvanaLogo from '../subcomponents/NirvanaLogo'; import { blueGrey } from '@mui/material/colors'; +import useSearch from '../providers/SearchProvider'; import useTerminal from './Terminal'; const Navbar = () => { - const [searchQuery, setSearchQuery] = useState(''); + const { searchQuery, omniSearch, conversationResults, userResults, isSearching } = useSearch(); const searchRef = useRef(null); const onSearchFocus = useCallback(() => { @@ -21,9 +22,9 @@ const Navbar = () => { const handleSearchChange = useCallback( (e: React.ChangeEvent) => { - setSearchQuery(e.target.value); + omniSearch(e.target.value); }, - [setSearchQuery], + [omniSearch], ); const rendersCount = useRendersCount(); @@ -68,7 +69,7 @@ const Navbar = () => { inputRef={searchRef} /> - {/* {isSearching && } */} + {isSearching && } {searchQuery ? ( diff --git a/packages/desktop/src/tree/NewConversationDialog.tsx b/packages/desktop/src/tree/NewConversationDialog.tsx new file mode 100644 index 0000000..9fc8a58 --- /dev/null +++ b/packages/desktop/src/tree/NewConversationDialog.tsx @@ -0,0 +1,193 @@ +import { + Autocomplete, + AutocompleteChangeReason, + AutocompleteRenderOptionState, + Button, + Container, + Dialog, + FormControl, + IconButton, + InputBase, + InputLabel, + ListItem, + Select, + Stack, + TextField, + Typography, + styled, +} from '@mui/material'; +import React, { HTMLAttributes, useCallback, useEffect, useState } from 'react'; +import { useDebounce, useKeyPressEvent, useToggle } from 'react-use'; +import useRouter, { EPage } from '../providers/RouterProvider'; + +import CircularProgress from '@mui/material/CircularProgress'; +import { FiX } from 'react-icons/fi'; +import { NirvanaRules } from '../util/rules'; +import User from '@nirvana/core/models/user.model'; +import UserDetailRow from '../subcomponents/UserDetailRow'; +import { blueGrey } from '@mui/material/colors'; +import toast from 'react-hot-toast'; +import useAuth from '../providers/AuthProvider'; +import useSearch from '../providers/SearchProvider'; + +export default function NewConversationDialog() { + const { page, setPage } = useRouter(); + + const { user } = useAuth(); + + const { userResults, searchQuery, searchUsers, isSearching } = useSearch(); + + const [selectedUsers, setSelectedUsers] = useState([]); + + const handleChangeSelections = useCallback( + (e: React.SyntheticEvent, value: User[], reason: AutocompleteChangeReason) => { + setSelectedUsers(value); + }, + [], + ); + + const handleChangeSearchInput = useCallback( + (e: React.ChangeEvent, newValue: string) => { + searchUsers(newValue); + }, + [searchUsers], + ); + + const renderOption = useCallback( + (props: HTMLAttributes, option: User, state: AutocompleteRenderOptionState) => ( + + + + ), + [], + ); + + const [conversationName, setConversationName] = useState(''); + + useEffect(() => { + if (selectedUsers.length < 2) { + setConversationName(''); + } + }, [selectedUsers, setConversationName]); + + const handleChangeName = useCallback( + (e: React.ChangeEvent) => { + setConversationName(e.target.value); + }, + [setConversationName], + ); + + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmitLocal = useCallback(async () => { + if (selectedUsers.length === 0) { + toast.error('Must select a person!'); + return; + } + + if (selectedUsers.length + 1 >= NirvanaRules.maxMembersPerConversation) { + toast.error('A group can only have 8 people including you!'); + return; + } + + setIsSubmitting(true); + + // await handleSubmit(selectedUsers, conversationName); + toast('submitting'); + + // clear form for next time + setSelectedUsers([]); + setConversationName(''); + + setIsSubmitting(false); + }, [selectedUsers, setConversationName, conversationName, setSelectedUsers, setIsSubmitting]); + + const handleClose = useCallback(() => { + setPage(EPage.WELCOME); + }, [setPage]); + + return ( + + + + + + + + Start a Conversation + + (typeof option === 'string' ? option : option.name)} + value={selectedUsers} + onChange={handleChangeSelections} + filterSelectedOptions + isOptionEqualToValue={(optionUser, valueUser) => optionUser._id.equals(valueUser._id)} + filterOptions={(options) => options} + inputValue={searchQuery} + renderInput={(params) => ( + + )} + /> + + {selectedUsers.length > 1 && ( + + )} + + + + + + + + + ); +} diff --git a/packages/desktop/src/tree/Terminal.tsx b/packages/desktop/src/tree/Terminal.tsx index abc26ce..be51f06 100644 --- a/packages/desktop/src/tree/Terminal.tsx +++ b/packages/desktop/src/tree/Terminal.tsx @@ -1,10 +1,15 @@ import { Box, Container, Grid } from '@mui/material'; +import { EPage } from '../providers/RouterProvider'; import Navbar from './Navbar'; +import NewConversationDialog from './NewConversationDialog'; import React from 'react'; import { blueGrey } from '@mui/material/colors'; +import useRouter from '../providers/RouterProvider'; export default function Terminal() { + const { page } = useRouter(); + return ( This is the footer controls + + {/* create chat dialog */} + ); } diff --git a/packages/desktop/src/tree/electronApp.tsx b/packages/desktop/src/tree/electronApp.tsx index 2a002cd..62e2564 100644 --- a/packages/desktop/src/tree/electronApp.tsx +++ b/packages/desktop/src/tree/electronApp.tsx @@ -3,6 +3,8 @@ import { ElectronProvider } from '../providers/ElectronProvider'; import { NirvanaTheme } from '../mui/NirvanaTheme'; import ProtectedRoute from './ProtectedRoute'; import React from 'react'; +import { RouterProvider } from '../providers/RouterProvider'; +import { SearchProvider } from '../providers/SearchProvider'; import { SocketProvider } from '../providers/SocketProvider'; import { StabilityProvider } from '../providers/StabilityProvider'; import Terminal from './Terminal'; @@ -19,7 +21,11 @@ export default function ElectronApp() { - + + + + +