refactor search all throughout

This commit is contained in:
talksik
2022-05-31 08:17:33 -05:00
parent 773ea9f38d
commit e4958342c0
8 changed files with 272 additions and 173 deletions
+1
View File
@@ -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 -3
View File
@@ -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>
+16 -27
View File
@@ -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
+11 -85
View File
@@ -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);
}
-50
View File
@@ -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 };
}