adding in search and router

This commit is contained in:
talksik
2022-06-11 06:35:23 -05:00
parent 818475234a
commit 57351d31c9
7 changed files with 391 additions and 6 deletions
+1 -1
View File
@@ -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
@@ -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<React.SetStateAction<EPage>>;
}
const RouterContext = React.createContext<IRouterContext>({
page: EPage.START_NEW_CONVERSATION,
});
export function RouterProvider({ children }: { children: React.ReactNode }) {
const [page, setPage] = useState<EPage>(EPage.START_NEW_CONVERSATION);
return <RouterContext.Provider value={{ page, setPage }}>{children}</RouterContext.Provider>;
}
export default function useRouter() {
return React.useContext(RouterContext);
}
@@ -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<ISearchContext>({
isSearching: false,
conversationResults: [],
userResults: [],
searchQuery: '',
});
export function SearchProvider({ children }: { children: React.ReactNode }) {
const { user } = useAuth();
// const { conversationMap } = useConversations();
const [searchQuery, setSearchQuery] = useState<string>('');
const [userResults, setUserResultsResults] = useState<User[]>([]);
const [conversationResults, setConversationResultsResults] = useState<Conversation[]>([]);
const [isSearching, setIsSearching] = useState<boolean>(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<Conversation[]> => {
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 (
<SearchContext.Provider
value={{
isSearching,
omniSearch,
searchUsers,
userResults,
conversationResults,
searchQuery,
clearSearch,
}}
>
{children}
</SearchContext.Provider>
);
}
export default function useSearch() {
return useContext(SearchContext);
}
+5 -4
View File
@@ -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<string>('');
const { searchQuery, omniSearch, conversationResults, userResults, isSearching } = useSearch();
const searchRef = useRef<HTMLInputElement>(null);
const onSearchFocus = useCallback(() => {
@@ -21,9 +22,9 @@ const Navbar = () => {
const handleSearchChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
omniSearch(e.target.value);
},
[setSearchQuery],
[omniSearch],
);
const rendersCount = useRendersCount();
@@ -68,7 +69,7 @@ const Navbar = () => {
inputRef={searchRef}
/>
{/* {isSearching && <CircularProgress size={20} />} */}
{isSearching && <CircularProgress size={20} />}
{searchQuery ? (
<KeyboardShortcutLabel label={KeyboardShortcuts.escape.label} />
@@ -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<User[]>([]);
const handleChangeSelections = useCallback(
(e: React.SyntheticEvent<Element, Event>, value: User[], reason: AutocompleteChangeReason) => {
setSelectedUsers(value);
},
[],
);
const handleChangeSearchInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>, newValue: string) => {
searchUsers(newValue);
},
[searchUsers],
);
const renderOption = useCallback(
(props: HTMLAttributes<HTMLLIElement>, option: User, state: AutocompleteRenderOptionState) => (
<ListItem {...props}>
<UserDetailRow user={option} />
</ListItem>
),
[],
);
const [conversationName, setConversationName] = useState<string>('');
useEffect(() => {
if (selectedUsers.length < 2) {
setConversationName('');
}
}, [selectedUsers, setConversationName]);
const handleChangeName = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setConversationName(e.target.value);
},
[setConversationName],
);
const [isSubmitting, setIsSubmitting] = useState<boolean>(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 (
<Dialog fullScreen open={page === EPage.START_NEW_CONVERSATION} onClose={handleClose}>
<Container
maxWidth={false}
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: 2,
flex: 1,
background: blueGrey[50],
position: 'relative',
}}
>
<IconButton sx={{ position: 'absolute', top: 10, right: 10 }} onClick={handleClose}>
<FiX />
</IconButton>
<Container
maxWidth="sm"
sx={{
display: 'flex',
flexDirection: 'column',
gap: 5,
}}
>
<Typography variant="h4">Start a Conversation</Typography>
<Autocomplete
multiple
loading={isSearching}
includeInputInList
id="tags-outlined"
autoHighlight
onInputChange={handleChangeSearchInput}
options={userResults}
renderOption={renderOption}
getOptionLabel={(option) => (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) => (
<TextField
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
fullWidth
label="People"
{...params}
placeholder="Search by name or email"
/>
)}
/>
{selectedUsers.length > 1 && (
<TextField
value={conversationName}
onChange={handleChangeName}
fullWidth
label="Name (optional)"
placeholder="Channel, subject line, whatever..."
/>
)}
<Stack justifyContent={'flex-end'} direction={'row'} spacing={2}>
<Button onClick={handleClose} variant={'text'}>
Cancel
</Button>
<Button
onClick={handleSubmitLocal}
disabled={selectedUsers.length === 0 || isSubmitting}
variant={'contained'}
color="primary"
>
{isSubmitting ? <CircularProgress /> : 'Connect'}
</Button>
</Stack>
</Container>
</Container>
</Dialog>
);
}
+8
View File
@@ -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 (
<Container
maxWidth={false}
@@ -47,6 +52,9 @@ export default function Terminal() {
>
This is the footer controls
</Box>
{/* create chat dialog */}
<NewConversationDialog />
</Container>
);
}
+7 -1
View File
@@ -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() {
<ProtectedRoute>
<ZenProvider>
<SocketProvider>
<Terminal />
<SearchProvider>
<RouterProvider>
<Terminal />
</RouterProvider>
</SearchProvider>
</SocketProvider>
</ZenProvider>
</ProtectedRoute>