refactor search all throughout
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
<AuthProvider>
|
||||
<ConversationProvider>
|
||||
<ZenProvider>
|
||||
<TerminalProvider>
|
||||
<></>
|
||||
</TerminalProvider>
|
||||
<SearchProvider>
|
||||
<TerminalProvider>
|
||||
<></>
|
||||
</TerminalProvider>
|
||||
</SearchProvider>
|
||||
</ZenProvider>
|
||||
</ConversationProvider>
|
||||
</AuthProvider>
|
||||
|
||||
@@ -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<HTMLInputElement>(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<HTMLInputElement>) => {
|
||||
omniSearch(e.target.value);
|
||||
},
|
||||
[omniSearch],
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -79,15 +68,15 @@ const Navbar = ({
|
||||
<FiSearch style={{ color: blueGrey[500] }} />
|
||||
|
||||
<Input
|
||||
onChange={handleChangeSearchInput}
|
||||
value={searchVal}
|
||||
onChange={handleSearchChange}
|
||||
value={searchQuery}
|
||||
placeholder={'Find or start a conversation'}
|
||||
inputRef={searchRef}
|
||||
/>
|
||||
|
||||
{isSearching && <CircularProgress size={20} />}
|
||||
|
||||
{searchVal ? (
|
||||
{searchQuery ? (
|
||||
<KeyboardShortcutLabel label={KeyboardShortcuts.escape.label} />
|
||||
) : (
|
||||
<KeyboardShortcutLabel label={KeyboardShortcuts.search.label} />
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<List
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { Box, Grid, Stack } from '@mui/material';
|
||||
import { ContentBlock, ContentType } from '@nirvana/core/src/models/content.model';
|
||||
import { ConversationContentMap, ConversationMap, UserMap } from '../util/types';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Unsubscribe, onSnapshot } from 'firebase/firestore';
|
||||
import {
|
||||
createOneOnOneConversation,
|
||||
getConversationContentQueryLIVE,
|
||||
getConversationsQueryLIVE,
|
||||
getUserById,
|
||||
searchUsers,
|
||||
sendContentBlockToConversation,
|
||||
} from '../firebase/firestore';
|
||||
import { useDebounce, useKeyPressEvent, useRendersCount, useUnmount } from 'react-use';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { createOneOnOneConversation, sendContentBlockToConversation } from '../firebase/firestore';
|
||||
import { useKeyPressEvent, useRendersCount } from 'react-use';
|
||||
|
||||
import Conversation from '@nirvana/core/src/models/conversation.model';
|
||||
import { ConversationList } from './ConversationList';
|
||||
import FooterControls from './FooterControls';
|
||||
import { KeyboardShortcuts } from '../util/keyboard';
|
||||
@@ -27,10 +17,8 @@ import { createGroupConversation } from '../firebase/firestore';
|
||||
import { uploadAudioClip } from '../firebase/firebaseStorage';
|
||||
import useAuth from '../providers/AuthProvider';
|
||||
import useConversations from '../providers/ConversationProvider';
|
||||
import { useImmer } from 'use-immer';
|
||||
import { useSearchConversations } from '../util/clientSearch';
|
||||
import useSearch from '../providers/SearchProvider';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import useStreamHandler from '../hooks/useStreamHandler';
|
||||
|
||||
interface ITerminalContext {
|
||||
handleQuickDial?: (otherUser: User) => 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<boolean>(false);
|
||||
@@ -235,54 +223,6 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
||||
|
||||
useKeyPressEvent('`', handleBroadcast, handleStopBroadcast);
|
||||
|
||||
const [searchVal, setSearchVal] = useState<string>('');
|
||||
const [searchUsersResults, setSearchUsersResults] = useState<User[]>([]);
|
||||
const [searchConversationsResults, setSearchConversationsResults] = useState<Conversation[]>([]);
|
||||
const [searching, setSearching] = useState<boolean>(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<HTMLInputElement>) => {
|
||||
handleOmniSearch(e.target.value);
|
||||
},
|
||||
[handleOmniSearch],
|
||||
);
|
||||
|
||||
const [createConversationMode, setCreateConversationMode] = useState<boolean>(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,
|
||||
}}
|
||||
>
|
||||
<Stack direction={'column'} sx={{ flex: 1 }}>
|
||||
@@ -362,22 +301,9 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Navbar
|
||||
searchVal={searchVal}
|
||||
isSearching={searching}
|
||||
handleChangeSearchInput={handleChangeSearchInput}
|
||||
/>
|
||||
<Navbar />
|
||||
|
||||
<Box sx={{ p: 2 }}>
|
||||
{searchVal ? (
|
||||
<OmniSearchResults
|
||||
people={searchUsersResults}
|
||||
conversations={searchConversationsResults}
|
||||
/>
|
||||
) : (
|
||||
<ConversationList />
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ p: 2 }}>{searchQuery ? <OmniSearchResults /> : <ConversationList />}</Box>
|
||||
</Grid>
|
||||
|
||||
<MainPanel />
|
||||
|
||||
@@ -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<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.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<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;
|
||||
},
|
||||
[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 (
|
||||
<SearchContext.Provider
|
||||
value={{
|
||||
isSearching,
|
||||
omniSearch,
|
||||
searchUsers,
|
||||
userResults,
|
||||
conversationResults,
|
||||
searchQuery,
|
||||
clearSearch,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SearchContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function useSearch() {
|
||||
return useContext(SearchContext);
|
||||
}
|
||||
|
||||
@@ -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<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;
|
||||
},
|
||||
[conversationMap],
|
||||
);
|
||||
|
||||
return { searchRelevantConversations };
|
||||
}
|
||||
Reference in New Issue
Block a user