adding in search and router
This commit is contained in:
Vendored
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"workbench.iconTheme": "material-icon-theme",
|
"workbench.iconTheme": "material-icon-theme",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"workbench.colorTheme": "Quiet Light",
|
"workbench.colorTheme": "Default Light+",
|
||||||
"javascript.format.semicolons": "insert",
|
"javascript.format.semicolons": "insert",
|
||||||
"window.zoomLevel": 1
|
"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);
|
||||||
|
}
|
||||||
@@ -7,10 +7,11 @@ import KeyboardShortcutLabel from '../subcomponents/KeyboardShortcutLabel';
|
|||||||
import { KeyboardShortcuts } from '../util/keyboard';
|
import { KeyboardShortcuts } from '../util/keyboard';
|
||||||
import NirvanaLogo from '../subcomponents/NirvanaLogo';
|
import NirvanaLogo from '../subcomponents/NirvanaLogo';
|
||||||
import { blueGrey } from '@mui/material/colors';
|
import { blueGrey } from '@mui/material/colors';
|
||||||
|
import useSearch from '../providers/SearchProvider';
|
||||||
import useTerminal from './Terminal';
|
import useTerminal from './Terminal';
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
const { searchQuery, omniSearch, conversationResults, userResults, isSearching } = useSearch();
|
||||||
|
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
const onSearchFocus = useCallback(() => {
|
const onSearchFocus = useCallback(() => {
|
||||||
@@ -21,9 +22,9 @@ const Navbar = () => {
|
|||||||
|
|
||||||
const handleSearchChange = useCallback(
|
const handleSearchChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setSearchQuery(e.target.value);
|
omniSearch(e.target.value);
|
||||||
},
|
},
|
||||||
[setSearchQuery],
|
[omniSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const rendersCount = useRendersCount();
|
const rendersCount = useRendersCount();
|
||||||
@@ -68,7 +69,7 @@ const Navbar = () => {
|
|||||||
inputRef={searchRef}
|
inputRef={searchRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* {isSearching && <CircularProgress size={20} />} */}
|
{isSearching && <CircularProgress size={20} />}
|
||||||
|
|
||||||
{searchQuery ? (
|
{searchQuery ? (
|
||||||
<KeyboardShortcutLabel label={KeyboardShortcuts.escape.label} />
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Box, Container, Grid } from '@mui/material';
|
import { Box, Container, Grid } from '@mui/material';
|
||||||
|
|
||||||
|
import { EPage } from '../providers/RouterProvider';
|
||||||
import Navbar from './Navbar';
|
import Navbar from './Navbar';
|
||||||
|
import NewConversationDialog from './NewConversationDialog';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { blueGrey } from '@mui/material/colors';
|
import { blueGrey } from '@mui/material/colors';
|
||||||
|
import useRouter from '../providers/RouterProvider';
|
||||||
|
|
||||||
export default function Terminal() {
|
export default function Terminal() {
|
||||||
|
const { page } = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
maxWidth={false}
|
maxWidth={false}
|
||||||
@@ -47,6 +52,9 @@ export default function Terminal() {
|
|||||||
>
|
>
|
||||||
This is the footer controls
|
This is the footer controls
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* create chat dialog */}
|
||||||
|
<NewConversationDialog />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { ElectronProvider } from '../providers/ElectronProvider';
|
|||||||
import { NirvanaTheme } from '../mui/NirvanaTheme';
|
import { NirvanaTheme } from '../mui/NirvanaTheme';
|
||||||
import ProtectedRoute from './ProtectedRoute';
|
import ProtectedRoute from './ProtectedRoute';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { RouterProvider } from '../providers/RouterProvider';
|
||||||
|
import { SearchProvider } from '../providers/SearchProvider';
|
||||||
import { SocketProvider } from '../providers/SocketProvider';
|
import { SocketProvider } from '../providers/SocketProvider';
|
||||||
import { StabilityProvider } from '../providers/StabilityProvider';
|
import { StabilityProvider } from '../providers/StabilityProvider';
|
||||||
import Terminal from './Terminal';
|
import Terminal from './Terminal';
|
||||||
@@ -19,7 +21,11 @@ export default function ElectronApp() {
|
|||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<ZenProvider>
|
<ZenProvider>
|
||||||
<SocketProvider>
|
<SocketProvider>
|
||||||
<Terminal />
|
<SearchProvider>
|
||||||
|
<RouterProvider>
|
||||||
|
<Terminal />
|
||||||
|
</RouterProvider>
|
||||||
|
</SearchProvider>
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
</ZenProvider>
|
</ZenProvider>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|||||||
Reference in New Issue
Block a user