cleaning up use of conversations and much nicer!
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { AuthProvider } from './providers/AuthProvider';
|
import { AuthProvider } from './providers/AuthProvider';
|
||||||
|
import { ConversationProvider } from './providers/ConversationProvider';
|
||||||
import { ElectronProvider } from './providers/ElectronProvider';
|
import { ElectronProvider } from './providers/ElectronProvider';
|
||||||
import ErrorParent from './providers/ErrorBoundary';
|
import ErrorParent from './providers/ErrorBoundary';
|
||||||
import { NirvanaTheme } from './mui/NirvanaTheme';
|
import { NirvanaTheme } from './mui/NirvanaTheme';
|
||||||
@@ -26,11 +27,13 @@ root.render(
|
|||||||
<ErrorParent>
|
<ErrorParent>
|
||||||
<ElectronProvider>
|
<ElectronProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ZenProvider>
|
<ConversationProvider>
|
||||||
<TerminalProvider>
|
<ZenProvider>
|
||||||
<></>
|
<TerminalProvider>
|
||||||
</TerminalProvider>
|
<></>
|
||||||
</ZenProvider>
|
</TerminalProvider>
|
||||||
|
</ZenProvider>
|
||||||
|
</ConversationProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ElectronProvider>
|
</ElectronProvider>
|
||||||
</ErrorParent>
|
</ErrorParent>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { User } from '@nirvana/core/src/models/user.model';
|
|||||||
/* eslint-disable jsx-a11y/media-has-caption */
|
/* eslint-disable jsx-a11y/media-has-caption */
|
||||||
import { blueGrey } from '@mui/material/colors';
|
import { blueGrey } from '@mui/material/colors';
|
||||||
import useAuth from '../providers/AuthProvider';
|
import useAuth from '../providers/AuthProvider';
|
||||||
|
import useConversations from '../providers/ConversationProvider';
|
||||||
import { useImmer } from 'use-immer';
|
import { useImmer } from 'use-immer';
|
||||||
import { useRendersCount } from 'react-use';
|
import { useRendersCount } from 'react-use';
|
||||||
import useTerminal from './Terminal';
|
import useTerminal from './Terminal';
|
||||||
@@ -32,13 +33,8 @@ import useTerminal from './Terminal';
|
|||||||
|
|
||||||
export default function ConversationDetails() {
|
export default function ConversationDetails() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const {
|
const { isCloudDoingMagic } = useTerminal();
|
||||||
getUser,
|
const { selectedConversation, conversationContentMap, selectConversation } = useConversations();
|
||||||
selectedConversation,
|
|
||||||
conversationContentMap,
|
|
||||||
selectConversation,
|
|
||||||
isCloudDoingMagic,
|
|
||||||
} = useTerminal();
|
|
||||||
|
|
||||||
const rendersCount = useRendersCount();
|
const rendersCount = useRendersCount();
|
||||||
console.warn('RENDER COUNT | CONVERSATION DETAILS | ', rendersCount);
|
console.warn('RENDER COUNT | CONVERSATION DETAILS | ', rendersCount);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import KeyboardShortcutLabel from './KeyboardShortcutLabel';
|
|||||||
import { NirvanaRules } from '../util/rules';
|
import { NirvanaRules } from '../util/rules';
|
||||||
import { SUPPORT_DISPLAY_NAME } from '../util/support';
|
import { SUPPORT_DISPLAY_NAME } from '../util/support';
|
||||||
import useAuth from '../providers/AuthProvider';
|
import useAuth from '../providers/AuthProvider';
|
||||||
|
import useConversations from '../providers/ConversationProvider';
|
||||||
import useTerminal from './Terminal';
|
import useTerminal from './Terminal';
|
||||||
|
|
||||||
// sort conversations based on the different data sources: type, conversations, audio clips, etc.
|
// sort conversations based on the different data sources: type, conversations, audio clips, etc.
|
||||||
@@ -33,8 +34,9 @@ import useTerminal from './Terminal';
|
|||||||
* @param lookingForSomeone: if a conversation id has been selected and we are looking for it
|
* @param lookingForSomeone: if a conversation id has been selected and we are looking for it
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function ConversationList({ lookingForSomeone = false }: { lookingForSomeone: boolean }) {
|
export function ConversationList() {
|
||||||
const { conversationMap, handleOmniSearch } = useTerminal();
|
const { handleOmniSearch } = useTerminal();
|
||||||
|
const { conversationMap } = useConversations();
|
||||||
|
|
||||||
const rendersCount = useRendersCount();
|
const rendersCount = useRendersCount();
|
||||||
console.warn('RENDER COUNT | CONVERSATION LIST | ', rendersCount);
|
console.warn('RENDER COUNT | CONVERSATION LIST | ', rendersCount);
|
||||||
@@ -69,8 +71,6 @@ export function ConversationList({ lookingForSomeone = false }: { lookingForSome
|
|||||||
</ListSubheader>
|
</ListSubheader>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{lookingForSomeone && <CircularProgress />}
|
|
||||||
|
|
||||||
{Object.values(conversationMap).map((currentConversation, index) => (
|
{Object.values(conversationMap).map((currentConversation, index) => (
|
||||||
<ConversationRow
|
<ConversationRow
|
||||||
key={`${currentConversation.id}-priorityConvoList`}
|
key={`${currentConversation.id}-priorityConvoList`}
|
||||||
@@ -110,7 +110,8 @@ export function ConversationRow({
|
|||||||
index?: number;
|
index?: number;
|
||||||
}) {
|
}) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { getUser, selectedConversation, selectConversation } = useTerminal();
|
|
||||||
|
const { selectedConversation, selectConversation } = useConversations();
|
||||||
|
|
||||||
const rendersCount = useRendersCount();
|
const rendersCount = useRendersCount();
|
||||||
console.warn('RENDER COUNT | CONVERSATION LIST ROW | ', rendersCount);
|
console.warn('RENDER COUNT | CONVERSATION LIST ROW | ', rendersCount);
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ import ConversationLabel from '../subcomponents/ConversationLabel';
|
|||||||
import { SUPPORT_DISPLAY_NAME } from '../util/support';
|
import { SUPPORT_DISPLAY_NAME } from '../util/support';
|
||||||
import { blueGrey } from '@mui/material/colors';
|
import { blueGrey } from '@mui/material/colors';
|
||||||
import useAuth from '../providers/AuthProvider';
|
import useAuth from '../providers/AuthProvider';
|
||||||
|
import useConversations from '../providers/ConversationProvider';
|
||||||
import useTerminal from './Terminal';
|
import useTerminal from './Terminal';
|
||||||
|
|
||||||
export default function FooterControls() {
|
export default function FooterControls() {
|
||||||
const { selectedConversation, handleOmniSearch } = useTerminal();
|
const { handleOmniSearch } = useTerminal();
|
||||||
|
const { selectedConversation } = useConversations();
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { Grid, Container, Typography } from '@mui/material';
|
import { Container, Grid, Typography } from '@mui/material';
|
||||||
import { blueGrey } from '@mui/material/colors';
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import useAuth from '../providers/AuthProvider';
|
|
||||||
import ConversationDetails from './ConversationDetails';
|
|
||||||
import useTerminal from './Terminal';
|
|
||||||
|
|
||||||
|
import ConversationDetails from './ConversationDetails';
|
||||||
|
import { blueGrey } from '@mui/material/colors';
|
||||||
|
import useAuth from '../providers/AuthProvider';
|
||||||
|
import useConversations from '../providers/ConversationProvider';
|
||||||
import { useRendersCount } from 'react-use';
|
import { useRendersCount } from 'react-use';
|
||||||
|
import useTerminal from './Terminal';
|
||||||
|
|
||||||
export default function MainPanel() {
|
export default function MainPanel() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { selectedConversation } = useTerminal();
|
|
||||||
|
const { selectedConversation } = useConversations();
|
||||||
|
|
||||||
// if selected conversation, show details
|
// if selected conversation, show details
|
||||||
// do all fetching necessary to paint things here
|
// do all fetching necessary to paint things here
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { ConversationRow } from './ConversationList';
|
|||||||
import { FiZap } from 'react-icons/fi';
|
import { FiZap } from 'react-icons/fi';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { User } from '@nirvana/core/src/models/user.model';
|
import { User } from '@nirvana/core/src/models/user.model';
|
||||||
|
import useConversations from '../providers/ConversationProvider';
|
||||||
import useTerminal from './Terminal';
|
import useTerminal from './Terminal';
|
||||||
|
|
||||||
export default function OmniSearchResults({
|
export default function OmniSearchResults({
|
||||||
@@ -25,7 +26,8 @@ export default function OmniSearchResults({
|
|||||||
people: User[];
|
people: User[];
|
||||||
conversations: Conversation[];
|
conversations: Conversation[];
|
||||||
}) {
|
}) {
|
||||||
const { handleQuickDial, selectConversation } = useTerminal();
|
const { handleQuickDial } = useTerminal();
|
||||||
|
const { selectConversation } = useConversations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -26,22 +26,15 @@ import { blueGrey } from '@mui/material/colors';
|
|||||||
import { createGroupConversation } from '../firebase/firestore';
|
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 useConversations from '../providers/ConversationProvider';
|
||||||
import { useImmer } from 'use-immer';
|
import { useImmer } from 'use-immer';
|
||||||
import { useSearchConversations } from '../util/clientSearch';
|
import { useSearchConversations } from '../util/clientSearch';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
import useStreamHandler from '../hooks/useStreamHandler';
|
import useStreamHandler from '../hooks/useStreamHandler';
|
||||||
|
|
||||||
interface ITerminalContext {
|
interface ITerminalContext {
|
||||||
conversationMap: ConversationMap;
|
|
||||||
conversationContentMap: ConversationContentMap;
|
|
||||||
|
|
||||||
selectedConversation?: Conversation;
|
|
||||||
selectConversation?: (conversationId: string) => void;
|
|
||||||
|
|
||||||
handleQuickDial?: (otherUser: User) => void;
|
handleQuickDial?: (otherUser: User) => void;
|
||||||
|
|
||||||
getUser?: (userId: string) => Promise<User | undefined>;
|
|
||||||
|
|
||||||
isUserSpeaking: boolean;
|
isUserSpeaking: boolean;
|
||||||
isCloudDoingMagic: boolean;
|
isCloudDoingMagic: boolean;
|
||||||
|
|
||||||
@@ -54,9 +47,6 @@ interface ITerminalContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TerminalContext = React.createContext<ITerminalContext>({
|
const TerminalContext = React.createContext<ITerminalContext>({
|
||||||
conversationMap: {},
|
|
||||||
conversationContentMap: {},
|
|
||||||
|
|
||||||
isUserSpeaking: false,
|
isUserSpeaking: false,
|
||||||
isCloudDoingMagic: false,
|
isCloudDoingMagic: false,
|
||||||
|
|
||||||
@@ -78,125 +68,11 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
const { user, logout, nirvanaUser } = useAuth();
|
const { user, logout, nirvanaUser } = useAuth();
|
||||||
|
|
||||||
const [selectedConversationId, setSelectedConversationId] = useState<string>(undefined);
|
const { conversationMap, conversationContentMap, selectConversation, selectedConversation } =
|
||||||
|
useConversations();
|
||||||
const [conversationMap, updateConversationMap] = useImmer<ConversationMap>({});
|
|
||||||
const [userMap, updateUserMap] = useImmer<UserMap>({});
|
|
||||||
const [conversationContentMap, updateContentMap] = useImmer<ConversationContentMap>({});
|
|
||||||
|
|
||||||
const { searchRelevantConversations } = useSearchConversations(conversationMap);
|
const { searchRelevantConversations } = useSearchConversations(conversationMap);
|
||||||
|
|
||||||
const [contentListeners, setContentListeners] = useImmer<{
|
|
||||||
[conversationId: string]: Unsubscribe;
|
|
||||||
}>({});
|
|
||||||
|
|
||||||
// TODO: put in a custom hook to separate out logic
|
|
||||||
const addConversationContentListener = useCallback(
|
|
||||||
(conversationId: string) => {
|
|
||||||
// ?optimization
|
|
||||||
// ? only start listening to conversation content where it is in my priority box?
|
|
||||||
// ? all other conversations, just fetch periodically?
|
|
||||||
|
|
||||||
// TODO: future work/optimization
|
|
||||||
// if we removed a conversation, unsubscribe from content listener as well
|
|
||||||
|
|
||||||
setContentListeners((draftListeners) => {
|
|
||||||
// if we have a listener for conversation already, then just move on
|
|
||||||
if (draftListeners[conversationId]) return;
|
|
||||||
|
|
||||||
// if we don't, then create one for this conversation
|
|
||||||
const contentListener = onSnapshot(
|
|
||||||
getConversationContentQueryLIVE(conversationId),
|
|
||||||
(querySnapshot) => {
|
|
||||||
querySnapshot.docChanges().forEach((docChange) => {
|
|
||||||
const currentContentBlock = docChange.doc.data();
|
|
||||||
|
|
||||||
if (docChange.type === 'added') {
|
|
||||||
// TODO: add to audio queue from here if there was an addition?
|
|
||||||
updateContentMap((draftContent) => {
|
|
||||||
if (draftContent[conversationId]) {
|
|
||||||
draftContent[conversationId].push(currentContentBlock);
|
|
||||||
} else {
|
|
||||||
draftContent[conversationId] = [currentContentBlock];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (docChange.type === 'modified') {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
if (docChange.type === 'removed') {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
//add to map of listeners
|
|
||||||
draftListeners[conversationId] = contentListener;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[setContentListeners, updateContentMap],
|
|
||||||
);
|
|
||||||
|
|
||||||
// fetch conversations
|
|
||||||
// TODO: put members map in another collection to avoid all of the re-renders for all folks...
|
|
||||||
// duplicate data for better reads
|
|
||||||
useEffect(() => {
|
|
||||||
const unsub = onSnapshot(getConversationsQueryLIVE(user.uid), (querySnapshot) => {
|
|
||||||
updateConversationMap((draft) => {
|
|
||||||
querySnapshot.docChanges().forEach((docChange) => {
|
|
||||||
const currentConversation = docChange.doc.data();
|
|
||||||
|
|
||||||
if (docChange.type === 'added') {
|
|
||||||
console.log('New conversation: ', currentConversation);
|
|
||||||
draft[currentConversation.id] = currentConversation;
|
|
||||||
|
|
||||||
addConversationContentListener(currentConversation.id);
|
|
||||||
}
|
|
||||||
if (docChange.type === 'modified') {
|
|
||||||
console.log('Modified conversation: ', currentConversation);
|
|
||||||
draft[currentConversation.id] = currentConversation;
|
|
||||||
}
|
|
||||||
if (docChange.type === 'removed') {
|
|
||||||
console.log('Removed conversation: ', currentConversation);
|
|
||||||
delete draft[currentConversation.id];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => unsub();
|
|
||||||
}, [user, updateConversationMap, enqueueSnackbar, addConversationContentListener]);
|
|
||||||
|
|
||||||
// on unmount, get rid of all content listeners
|
|
||||||
useUnmount(() => {
|
|
||||||
Object.values(contentListeners).forEach((unsub) => unsub());
|
|
||||||
});
|
|
||||||
|
|
||||||
// cache of selected conversation
|
|
||||||
const selectedConversation: Conversation | undefined = useMemo(() => {
|
|
||||||
if (!selectedConversationId) return undefined;
|
|
||||||
|
|
||||||
// select if we do have it
|
|
||||||
|
|
||||||
const conversation = conversationMap[selectedConversationId];
|
|
||||||
|
|
||||||
if (!conversation) {
|
|
||||||
enqueueSnackbar('no conversation found', { variant: 'error' });
|
|
||||||
console.error('Should have found it... maybe retry once more');
|
|
||||||
}
|
|
||||||
|
|
||||||
return conversation;
|
|
||||||
|
|
||||||
// get conversation if not here
|
|
||||||
// const fetchedConversation = await getConversationById
|
|
||||||
|
|
||||||
// return undefined;
|
|
||||||
}, [selectedConversationId, conversationMap, enqueueSnackbar]);
|
|
||||||
|
|
||||||
// handles stream and device stuff
|
|
||||||
useStreamHandler(selectedConversation);
|
|
||||||
|
|
||||||
// handle create or open existing conversation
|
// handle create or open existing conversation
|
||||||
// not 100% consistent to the second, but still works...don't need atomicity
|
// not 100% consistent to the second, but still works...don't need atomicity
|
||||||
const handleQuickDial = useCallback(
|
const handleQuickDial = useCallback(
|
||||||
@@ -219,33 +95,18 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (findExistingConversation) {
|
if (findExistingConversation) {
|
||||||
setSelectedConversationId(findExistingConversation.id);
|
selectConversation(findExistingConversation.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create conversation in this case
|
// create conversation in this case
|
||||||
const newConversationId = await createOneOnOneConversation(otherUser, nirvanaUser);
|
const newConversationId = await createOneOnOneConversation(otherUser, nirvanaUser);
|
||||||
setSelectedConversationId(newConversationId);
|
selectConversation(newConversationId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
enqueueSnackbar('Something went wrong, please try again', { variant: 'error' });
|
enqueueSnackbar('Something went wrong, please try again', { variant: 'error' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[conversationMap, enqueueSnackbar, nirvanaUser],
|
[conversationMap, enqueueSnackbar, nirvanaUser, selectConversation],
|
||||||
);
|
|
||||||
|
|
||||||
const getUser = useCallback(
|
|
||||||
async (userId: string) => {
|
|
||||||
if (userMap[userId]) return userMap[userId];
|
|
||||||
|
|
||||||
const fetchedUser = await getUserById(userId);
|
|
||||||
|
|
||||||
updateUserMap((draft) => {
|
|
||||||
draft[userId] = fetchedUser;
|
|
||||||
});
|
|
||||||
|
|
||||||
return fetchedUser;
|
|
||||||
},
|
|
||||||
[userMap, updateUserMap],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isUserSpeaking, setIsUserSpeaking] = useState<boolean>(false);
|
const [isUserSpeaking, setIsUserSpeaking] = useState<boolean>(false);
|
||||||
@@ -423,11 +284,6 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
[handleOmniSearch],
|
[handleOmniSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectConversation = useCallback(
|
|
||||||
(conversationId: string) => setSelectedConversationId(conversationId),
|
|
||||||
[setSelectedConversationId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [createConversationMode, setCreateConversationMode] = useState<boolean>(false);
|
const [createConversationMode, setCreateConversationMode] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleStartConversation = useCallback(
|
const handleStartConversation = useCallback(
|
||||||
@@ -453,29 +309,29 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
nirvanaUser,
|
nirvanaUser,
|
||||||
conversationName ?? null,
|
conversationName ?? null,
|
||||||
);
|
);
|
||||||
setSelectedConversationId(newConversationId);
|
selectConversation(newConversationId);
|
||||||
|
|
||||||
setCreateConversationMode(false);
|
setCreateConversationMode(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
enqueueSnackbar('Something went wrong, please try again', { variant: 'error' });
|
enqueueSnackbar('Something went wrong, please try again', { variant: 'error' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[nirvanaUser, handleQuickDial, enqueueSnackbar, setCreateConversationMode],
|
[nirvanaUser, handleQuickDial, enqueueSnackbar, setCreateConversationMode, selectConversation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleShowCreateConvoForm = useCallback(() => {
|
const handleShowCreateConvoForm = useCallback(() => {
|
||||||
setSelectedConversationId(undefined);
|
selectConversation(undefined);
|
||||||
setCreateConversationMode(true);
|
setCreateConversationMode(true);
|
||||||
}, [setCreateConversationMode, setSelectedConversationId]);
|
}, [setCreateConversationMode, selectConversation]);
|
||||||
|
|
||||||
const rendersCount = useRendersCount();
|
const rendersCount = useRendersCount();
|
||||||
console.warn('RENDER COUNT | TERMINAL | ', rendersCount);
|
console.warn('RENDER COUNT | TERMINAL | ', rendersCount);
|
||||||
|
|
||||||
const handleEscape = useCallback(() => {
|
const handleEscape = useCallback(() => {
|
||||||
setCreateConversationMode(false);
|
setCreateConversationMode(false);
|
||||||
setSelectedConversationId(undefined);
|
selectConversation(undefined);
|
||||||
setSearchVal('');
|
setSearchVal('');
|
||||||
}, [setSelectedConversationId, setCreateConversationMode, setSearchVal]);
|
}, [selectConversation, setCreateConversationMode, setSearchVal]);
|
||||||
|
|
||||||
useKeyPressEvent(KeyboardShortcuts.escape.shortcutKey, handleEscape);
|
useKeyPressEvent(KeyboardShortcuts.escape.shortcutKey, handleEscape);
|
||||||
|
|
||||||
@@ -483,13 +339,8 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
<TerminalContext.Provider
|
<TerminalContext.Provider
|
||||||
value={{
|
value={{
|
||||||
isUserSpeaking,
|
isUserSpeaking,
|
||||||
selectedConversation,
|
|
||||||
conversationMap,
|
|
||||||
getUser,
|
|
||||||
handleQuickDial,
|
handleQuickDial,
|
||||||
selectConversation,
|
|
||||||
isCloudDoingMagic,
|
isCloudDoingMagic,
|
||||||
conversationContentMap,
|
|
||||||
createConversationMode,
|
createConversationMode,
|
||||||
handleShowCreateConvoForm,
|
handleShowCreateConvoForm,
|
||||||
handleEscape,
|
handleEscape,
|
||||||
@@ -525,9 +376,7 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
conversations={searchConversationsResults}
|
conversations={searchConversationsResults}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConversationList
|
<ConversationList />
|
||||||
lookingForSomeone={selectedConversationId && !selectedConversation}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,13 +1,184 @@
|
|||||||
import { ConversationContentMap, ConversationMap } from '../util/types';
|
import { ConversationContentMap, ConversationMap, UserMap } from '../util/types';
|
||||||
|
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Unsubscribe, onSnapshot } from 'firebase/firestore';
|
||||||
|
import {
|
||||||
|
getConversationContentQueryLIVE,
|
||||||
|
getConversationsQueryLIVE,
|
||||||
|
getUserById,
|
||||||
|
} from '../firebase/firestore';
|
||||||
|
|
||||||
import React from 'react';
|
import Conversation from '@nirvana/core/src/models/conversation.model';
|
||||||
|
import { User } from '@nirvana/core/src/models/user.model';
|
||||||
|
import useAuth from './AuthProvider';
|
||||||
|
import { useImmer } from 'use-immer';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
import { useUnmount } from 'react-use';
|
||||||
|
|
||||||
interface IConversationContext {
|
interface IConversationContext {
|
||||||
conversationMap: ConversationMap;
|
conversationMap: ConversationMap;
|
||||||
conversationContentMap: ConversationContentMap;
|
conversationContentMap: ConversationContentMap;
|
||||||
|
|
||||||
|
selectedConversation?: Conversation;
|
||||||
|
selectConversation?: (conversationId: string) => void;
|
||||||
|
|
||||||
|
getUser?: (userId: string) => Promise<User | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConversationContext = React.createContext<IConversationContext>({
|
const ConversationContext = React.createContext<IConversationContext>({
|
||||||
conversationContentMap: {},
|
conversationContentMap: {},
|
||||||
conversationMap: {},
|
conversationMap: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function ConversationProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const [selectedConversationId, setSelectedConversationId] = useState<string>(undefined);
|
||||||
|
|
||||||
|
const [conversationMap, updateConversationMap] = useImmer<ConversationMap>({});
|
||||||
|
const [userMap, updateUserMap] = useImmer<UserMap>({});
|
||||||
|
const [conversationContentMap, updateContentMap] = useImmer<ConversationContentMap>({});
|
||||||
|
|
||||||
|
const [contentListeners, setContentListeners] = useImmer<{
|
||||||
|
[conversationId: string]: Unsubscribe;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
const addConversationContentListener = useCallback(
|
||||||
|
(conversationId: string) => {
|
||||||
|
// ?optimization
|
||||||
|
// ? only start listening to conversation content where it is in my priority box?
|
||||||
|
// ? all other conversations, just fetch periodically?
|
||||||
|
|
||||||
|
// TODO: future work/optimization
|
||||||
|
// if we removed a conversation, unsubscribe from content listener as well
|
||||||
|
|
||||||
|
setContentListeners((draftListeners) => {
|
||||||
|
// if we have a listener for conversation already, then just move on
|
||||||
|
if (draftListeners[conversationId]) return;
|
||||||
|
|
||||||
|
// if we don't, then create one for this conversation
|
||||||
|
const contentListener = onSnapshot(
|
||||||
|
getConversationContentQueryLIVE(conversationId),
|
||||||
|
(querySnapshot) => {
|
||||||
|
querySnapshot.docChanges().forEach((docChange) => {
|
||||||
|
const currentContentBlock = docChange.doc.data();
|
||||||
|
|
||||||
|
if (docChange.type === 'added') {
|
||||||
|
// TODO: add to audio queue from here if there was an addition?
|
||||||
|
updateContentMap((draftContent) => {
|
||||||
|
if (draftContent[conversationId]) {
|
||||||
|
draftContent[conversationId].push(currentContentBlock);
|
||||||
|
} else {
|
||||||
|
draftContent[conversationId] = [currentContentBlock];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (docChange.type === 'modified') {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
if (docChange.type === 'removed') {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
//add to map of listeners
|
||||||
|
draftListeners[conversationId] = contentListener;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setContentListeners, updateContentMap],
|
||||||
|
);
|
||||||
|
// on unmount, get rid of all content listeners
|
||||||
|
useUnmount(() => {
|
||||||
|
Object.values(contentListeners).forEach((unsub) => unsub());
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetch conversations
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = onSnapshot(getConversationsQueryLIVE(user.uid), (querySnapshot) => {
|
||||||
|
updateConversationMap((draft) => {
|
||||||
|
querySnapshot.docChanges().forEach((docChange) => {
|
||||||
|
const currentConversation = docChange.doc.data();
|
||||||
|
|
||||||
|
if (docChange.type === 'added') {
|
||||||
|
console.log('New conversation: ', currentConversation);
|
||||||
|
draft[currentConversation.id] = currentConversation;
|
||||||
|
|
||||||
|
addConversationContentListener(currentConversation.id);
|
||||||
|
}
|
||||||
|
if (docChange.type === 'modified') {
|
||||||
|
console.log('Modified conversation: ', currentConversation);
|
||||||
|
draft[currentConversation.id] = currentConversation;
|
||||||
|
}
|
||||||
|
if (docChange.type === 'removed') {
|
||||||
|
console.log('Removed conversation: ', currentConversation);
|
||||||
|
delete draft[currentConversation.id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unsub();
|
||||||
|
}, [user, updateConversationMap, enqueueSnackbar, addConversationContentListener]);
|
||||||
|
|
||||||
|
const getUser = useCallback(
|
||||||
|
async (userId: string) => {
|
||||||
|
if (userMap[userId]) return userMap[userId];
|
||||||
|
|
||||||
|
const fetchedUser = await getUserById(userId);
|
||||||
|
|
||||||
|
updateUserMap((draft) => {
|
||||||
|
draft[userId] = fetchedUser;
|
||||||
|
});
|
||||||
|
|
||||||
|
return fetchedUser;
|
||||||
|
},
|
||||||
|
[userMap, updateUserMap],
|
||||||
|
);
|
||||||
|
|
||||||
|
// cache of selected conversation
|
||||||
|
const selectedConversation: Conversation | undefined = useMemo(() => {
|
||||||
|
if (!selectedConversationId) return undefined;
|
||||||
|
|
||||||
|
// select if we do have it
|
||||||
|
|
||||||
|
const conversation = conversationMap[selectedConversationId];
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
enqueueSnackbar('no conversation found', { variant: 'error' });
|
||||||
|
console.error('Should have found it... maybe retry once more');
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversation;
|
||||||
|
|
||||||
|
// get conversation if not here
|
||||||
|
// const fetchedConversation = await getConversationById
|
||||||
|
|
||||||
|
// return undefined;
|
||||||
|
}, [selectedConversationId, conversationMap, enqueueSnackbar]);
|
||||||
|
|
||||||
|
const selectConversation = useCallback(
|
||||||
|
(conversationId: string) => setSelectedConversationId(conversationId),
|
||||||
|
[setSelectedConversationId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConversationContext.Provider
|
||||||
|
value={{
|
||||||
|
conversationMap,
|
||||||
|
conversationContentMap,
|
||||||
|
selectedConversation,
|
||||||
|
selectConversation,
|
||||||
|
getUser,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ConversationContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useConversations() {
|
||||||
|
return useContext(ConversationContext);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user