clean stuff with client side search and working filtering conversations list
This commit is contained in:
@@ -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 { user } = useAuth();
|
||||||
const { getUser, selectedConversation, selectConversation } = useTerminal();
|
const { getUser, selectedConversation, selectConversation } = useTerminal();
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { ContentBlock, ContentType } from '@nirvana/core/src/models/content.model';
|
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 { FiHeadphones, FiLogOut, FiMonitor, FiSun, FiZap } from 'react-icons/fi';
|
||||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { Unsubscribe, onSnapshot } from 'firebase/firestore';
|
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 Conversation from '@nirvana/core/src/models/conversation.model';
|
||||||
import ConversationLabel from '../subcomponents/ConversationLabel';
|
import ConversationLabel from '../subcomponents/ConversationLabel';
|
||||||
import { ConversationList } from './ConversationList';
|
|
||||||
import FooterControls from './FooterControls';
|
import FooterControls from './FooterControls';
|
||||||
import MainPanel from './MainPanel';
|
import MainPanel from './MainPanel';
|
||||||
import Navbar from './Navbar';
|
import Navbar from './Navbar';
|
||||||
@@ -50,20 +51,9 @@ import { createGroupConversation } from '../firebase/firestore';
|
|||||||
import { uploadAudioClip } from '../firebase/firebaseStorage';
|
import { uploadAudioClip } from '../firebase/firebaseStorage';
|
||||||
import useAuth from '../providers/AuthProvider';
|
import useAuth from '../providers/AuthProvider';
|
||||||
import { useImmer } from 'use-immer';
|
import { useImmer } from 'use-immer';
|
||||||
|
import { useSearchConversations } from '../util/clientSearch';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
type ConversationMap = {
|
|
||||||
[conversationId: string]: Conversation;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UserMap = {
|
|
||||||
[userId: string]: User;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ConversationContentMap = {
|
|
||||||
[conversationId: string]: ContentBlock[];
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ITerminalContext {
|
interface ITerminalContext {
|
||||||
conversationMap: ConversationMap;
|
conversationMap: ConversationMap;
|
||||||
conversationContentMap: ConversationContentMap;
|
conversationContentMap: ConversationContentMap;
|
||||||
@@ -117,6 +107,8 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
const [userMap, updateUserMap] = useImmer<UserMap>({});
|
const [userMap, updateUserMap] = useImmer<UserMap>({});
|
||||||
const [conversationContentMap, updateContentMap] = useImmer<ConversationContentMap>({});
|
const [conversationContentMap, updateContentMap] = useImmer<ConversationContentMap>({});
|
||||||
|
|
||||||
|
const { searchRelevantConversations } = useSearchConversations(conversationMap);
|
||||||
|
|
||||||
const [contentListeners, setContentListeners] = useImmer<{
|
const [contentListeners, setContentListeners] = useImmer<{
|
||||||
[conversationId: string]: Unsubscribe;
|
[conversationId: string]: Unsubscribe;
|
||||||
}>({});
|
}>({});
|
||||||
@@ -413,8 +405,10 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
|
|
||||||
const [searchVal, setSearchVal] = useState<string>('');
|
const [searchVal, setSearchVal] = useState<string>('');
|
||||||
const [searchUsersResults, setSearchUsersResults] = useState<User[]>([]);
|
const [searchUsersResults, setSearchUsersResults] = useState<User[]>([]);
|
||||||
|
const [searchConversationsResults, setSearchConversationsResults] = useState<Conversation[]>([]);
|
||||||
const [searching, setSearching] = useState<boolean>(false);
|
const [searching, setSearching] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// debounce user search
|
||||||
const [, cancel] = useDebounce(
|
const [, cancel] = useDebounce(
|
||||||
async () => {
|
async () => {
|
||||||
if (searchVal) {
|
if (searchVal) {
|
||||||
@@ -432,15 +426,22 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleOmniSearch = useCallback(
|
const handleOmniSearch = useCallback(
|
||||||
(searchQuery: string) => {
|
async (searchQuery: string) => {
|
||||||
|
searchQuery = searchQuery.replace('/', '');
|
||||||
|
setSearchVal(searchQuery);
|
||||||
|
|
||||||
if (!searchQuery) {
|
if (!searchQuery) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// user search
|
||||||
setSearching(true);
|
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(
|
const handleChangeSearchInput = useCallback(
|
||||||
@@ -546,7 +547,10 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
|
|
||||||
<Box sx={{ p: 2 }}>
|
<Box sx={{ p: 2 }}>
|
||||||
{searchVal ? (
|
{searchVal ? (
|
||||||
<ListPeople people={searchUsersResults} />
|
<SearchResults
|
||||||
|
people={searchUsersResults}
|
||||||
|
conversations={searchConversationsResults}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConversationList
|
<ConversationList
|
||||||
lookingForSomeone={selectedConversationId && !selectedConversation}
|
lookingForSomeone={selectedConversationId && !selectedConversation}
|
||||||
@@ -577,58 +581,101 @@ export default function useTerminal() {
|
|||||||
return useContext(TerminalContext);
|
return useContext(TerminalContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListPeople({ people }: { people: User[] }) {
|
function SearchResults({
|
||||||
const { handleQuickDial } = useTerminal();
|
people,
|
||||||
|
conversations,
|
||||||
|
}: {
|
||||||
|
people: User[];
|
||||||
|
conversations: Conversation[];
|
||||||
|
}) {
|
||||||
|
const { handleQuickDial, selectConversation } = useTerminal();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<>
|
||||||
sx={{
|
<List
|
||||||
pt: 2,
|
sx={{
|
||||||
}}
|
pt: 2,
|
||||||
subheader={
|
}}
|
||||||
<ListSubheader
|
subheader={
|
||||||
|
<ListSubheader
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography align={'center'} variant="subtitle2">
|
||||||
|
People
|
||||||
|
</Typography>
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItem
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography align={'center'} variant="subtitle2">
|
{people.length === 0 && (
|
||||||
Search Results
|
<Typography align="center" variant="caption">
|
||||||
</Typography>
|
{`Can't find someone? Invite them!`}
|
||||||
</ListSubheader>
|
</Typography>
|
||||||
}
|
)}
|
||||||
>
|
</ListItem>
|
||||||
<ListItem
|
|
||||||
|
{people.map((person) => (
|
||||||
|
<Tooltip key={`${person.uid}-searchUsers`} title={'quick dial'}>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemButton onClick={() => handleQuickDial(person)}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar alt={person.displayName} src={person.photoUrl} />
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary={person.displayName} secondary={person.email} />
|
||||||
|
|
||||||
|
<ListItemSecondaryAction sx={{ color: 'GrayText' }}>
|
||||||
|
<FiZap />
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<List
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
pt: 2,
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
|
subheader={
|
||||||
|
<ListSubheader
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography align={'center'} variant="subtitle2">
|
||||||
|
Conversations
|
||||||
|
</Typography>
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{people.length === 0 && (
|
<ListItem
|
||||||
<Typography align="center" variant="caption">
|
sx={{
|
||||||
{`Can't find someone? Invite them!`}
|
display: 'flex',
|
||||||
</Typography>
|
justifyContent: 'center',
|
||||||
)}
|
}}
|
||||||
</ListItem>
|
>
|
||||||
|
{conversations.length === 0 && (
|
||||||
|
<Typography align="center" variant="caption">
|
||||||
|
{`Can't find a conversation? Just create one!`}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
{people.map((person) => (
|
{conversations.map((conversation) => (
|
||||||
<Tooltip key={`${person.uid}-searchUsers`} title={'quick dial'}>
|
<Tooltip key={`${conversation.id}-searchConversations`} title={'continue conversation'}>
|
||||||
<ListItem>
|
<ConversationRow conversation={conversation} />
|
||||||
<ListItemButton onClick={() => handleQuickDial(person)}>
|
</Tooltip>
|
||||||
<ListItemAvatar>
|
))}
|
||||||
<Avatar alt={person.displayName} src={person.photoUrl} />
|
</List>
|
||||||
</ListItemAvatar>
|
</>
|
||||||
<ListItemText primary={person.displayName} secondary={person.email} />
|
|
||||||
|
|
||||||
<ListItemSecondaryAction sx={{ color: 'GrayText' }}>
|
|
||||||
<FiZap />
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxPriorityConvos = 5;
|
|
||||||
|
|||||||
@@ -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<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 };
|
||||||
|
}
|
||||||
@@ -2,4 +2,6 @@
|
|||||||
|
|
||||||
export const NirvanaRules = {
|
export const NirvanaRules = {
|
||||||
maxMembersPerConversation: 8,
|
maxMembersPerConversation: 8,
|
||||||
|
|
||||||
|
maxPriorityConvos: 5,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user