diff --git a/packages/desktop/src/components/ConversationList.tsx b/packages/desktop/src/components/ConversationList.tsx index dd89bfd..f23b295 100644 --- a/packages/desktop/src/components/ConversationList.tsx +++ b/packages/desktop/src/components/ConversationList.tsx @@ -94,7 +94,7 @@ export function ConversationList({ lookingForSomeone = false }: { lookingForSome ); } -function ConversationRow({ conversation }: { conversation: Conversation }) { +export function ConversationRow({ conversation }: { conversation: Conversation }) { const { user } = useAuth(); const { getUser, selectedConversation, selectConversation } = useTerminal(); diff --git a/packages/desktop/src/components/Terminal.tsx b/packages/desktop/src/components/Terminal.tsx index 7d2fff9..1181df6 100644 --- a/packages/desktop/src/components/Terminal.tsx +++ b/packages/desktop/src/components/Terminal.tsx @@ -24,6 +24,8 @@ import { Typography, } from '@mui/material'; import { ContentBlock, ContentType } from '@nirvana/core/src/models/content.model'; +import { ConversationContentMap, ConversationMap, UserMap } from '../util/types'; +import { ConversationList, ConversationRow } from './ConversationList'; import { FiHeadphones, FiLogOut, FiMonitor, FiSun, FiZap } from 'react-icons/fi'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { Unsubscribe, onSnapshot } from 'firebase/firestore'; @@ -39,7 +41,6 @@ import { useDebounce, useKeyPressEvent, useRendersCount, useUnmount } from 'reac import Conversation from '@nirvana/core/src/models/conversation.model'; import ConversationLabel from '../subcomponents/ConversationLabel'; -import { ConversationList } from './ConversationList'; import FooterControls from './FooterControls'; import MainPanel from './MainPanel'; import Navbar from './Navbar'; @@ -50,20 +51,9 @@ import { createGroupConversation } from '../firebase/firestore'; import { uploadAudioClip } from '../firebase/firebaseStorage'; import useAuth from '../providers/AuthProvider'; import { useImmer } from 'use-immer'; +import { useSearchConversations } from '../util/clientSearch'; import { useSnackbar } from 'notistack'; -type ConversationMap = { - [conversationId: string]: Conversation; -}; - -type UserMap = { - [userId: string]: User; -}; - -type ConversationContentMap = { - [conversationId: string]: ContentBlock[]; -}; - interface ITerminalContext { conversationMap: ConversationMap; conversationContentMap: ConversationContentMap; @@ -117,6 +107,8 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { const [userMap, updateUserMap] = useImmer({}); const [conversationContentMap, updateContentMap] = useImmer({}); + const { searchRelevantConversations } = useSearchConversations(conversationMap); + const [contentListeners, setContentListeners] = useImmer<{ [conversationId: string]: Unsubscribe; }>({}); @@ -413,8 +405,10 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { const [searchVal, setSearchVal] = useState(''); const [searchUsersResults, setSearchUsersResults] = useState([]); + const [searchConversationsResults, setSearchConversationsResults] = useState([]); const [searching, setSearching] = useState(false); + // debounce user search const [, cancel] = useDebounce( async () => { if (searchVal) { @@ -432,15 +426,22 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { ); const handleOmniSearch = useCallback( - (searchQuery: string) => { + async (searchQuery: string) => { + searchQuery = searchQuery.replace('/', ''); + setSearchVal(searchQuery); + if (!searchQuery) { return; } + // user search setSearching(true); - setSearchVal(searchQuery.replace('/', '')); + + // client side conversation search + const relevantConversations = await searchRelevantConversations(searchQuery); + setSearchConversationsResults(relevantConversations); }, - [setSearching, setSearchVal], + [setSearching, setSearchVal, setSearchConversationsResults, searchRelevantConversations], ); const handleChangeSearchInput = useCallback( @@ -546,7 +547,10 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { {searchVal ? ( - + ) : ( + + + People + + + } + > + - - Search Results - - - } - > - + {`Can't find someone? Invite them!`} + + )} + + + {people.map((person) => ( + + + handleQuickDial(person)}> + + + + + + + + + + + + ))} + + + + + Conversations + + + } > - {people.length === 0 && ( - - {`Can't find someone? Invite them!`} - - )} - + + {conversations.length === 0 && ( + + {`Can't find a conversation? Just create one!`} + + )} + - {people.map((person) => ( - - - handleQuickDial(person)}> - - - - - - - - - - - - ))} - + {conversations.map((conversation) => ( + + + + ))} + + ); } - -const maxPriorityConvos = 5; diff --git a/packages/desktop/src/util/clientSearch.ts b/packages/desktop/src/util/clientSearch.ts new file mode 100644 index 0000000..2418680 --- /dev/null +++ b/packages/desktop/src/util/clientSearch.ts @@ -0,0 +1,50 @@ +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 => { + 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 }; +} diff --git a/packages/desktop/src/util/rules.ts b/packages/desktop/src/util/rules.ts index 03a129e..87a47e6 100644 --- a/packages/desktop/src/util/rules.ts +++ b/packages/desktop/src/util/rules.ts @@ -2,4 +2,6 @@ export const NirvanaRules = { maxMembersPerConversation: 8, + + maxPriorityConvos: 5, }; diff --git a/packages/desktop/src/util/types.ts b/packages/desktop/src/util/types.ts new file mode 100644 index 0000000..54499dc --- /dev/null +++ b/packages/desktop/src/util/types.ts @@ -0,0 +1,15 @@ +import { ContentBlock } from '@nirvana/core/src/models/content.model'; +import Conversation from '@nirvana/core/src/models/conversation.model'; +import { User } from '@nirvana/core/src/models/user.model'; + +export type ConversationMap = { + [conversationId: string]: Conversation; +}; + +export type UserMap = { + [userId: string]: User; +}; + +export type ConversationContentMap = { + [conversationId: string]: ContentBlock[]; +};