diff --git a/packages/desktop/src/app.tsx b/packages/desktop/src/app.tsx
index a9b14d8..670cf93 100644
--- a/packages/desktop/src/app.tsx
+++ b/packages/desktop/src/app.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { AuthProvider } from './providers/AuthProvider';
+import { ConversationProvider } from './providers/ConversationProvider';
import { ElectronProvider } from './providers/ElectronProvider';
import ErrorParent from './providers/ErrorBoundary';
import { NirvanaTheme } from './mui/NirvanaTheme';
@@ -26,11 +27,13 @@ root.render(
-
-
- <>>
-
-
+
+
+
+ <>>
+
+
+
diff --git a/packages/desktop/src/components/ConversationDetails.tsx b/packages/desktop/src/components/ConversationDetails.tsx
index 5b47cdf..79a59e8 100644
--- a/packages/desktop/src/components/ConversationDetails.tsx
+++ b/packages/desktop/src/components/ConversationDetails.tsx
@@ -19,6 +19,7 @@ import { User } from '@nirvana/core/src/models/user.model';
/* eslint-disable jsx-a11y/media-has-caption */
import { blueGrey } from '@mui/material/colors';
import useAuth from '../providers/AuthProvider';
+import useConversations from '../providers/ConversationProvider';
import { useImmer } from 'use-immer';
import { useRendersCount } from 'react-use';
import useTerminal from './Terminal';
@@ -32,13 +33,8 @@ import useTerminal from './Terminal';
export default function ConversationDetails() {
const { user } = useAuth();
- const {
- getUser,
- selectedConversation,
- conversationContentMap,
- selectConversation,
- isCloudDoingMagic,
- } = useTerminal();
+ const { isCloudDoingMagic } = useTerminal();
+ const { selectedConversation, conversationContentMap, selectConversation } = useConversations();
const rendersCount = useRendersCount();
console.warn('RENDER COUNT | CONVERSATION DETAILS | ', rendersCount);
diff --git a/packages/desktop/src/components/ConversationList.tsx b/packages/desktop/src/components/ConversationList.tsx
index c6c66bc..ca35e1f 100644
--- a/packages/desktop/src/components/ConversationList.tsx
+++ b/packages/desktop/src/components/ConversationList.tsx
@@ -24,6 +24,7 @@ import KeyboardShortcutLabel from './KeyboardShortcutLabel';
import { NirvanaRules } from '../util/rules';
import { SUPPORT_DISPLAY_NAME } from '../util/support';
import useAuth from '../providers/AuthProvider';
+import useConversations from '../providers/ConversationProvider';
import useTerminal from './Terminal';
// 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
* @returns
*/
-export function ConversationList({ lookingForSomeone = false }: { lookingForSomeone: boolean }) {
- const { conversationMap, handleOmniSearch } = useTerminal();
+export function ConversationList() {
+ const { handleOmniSearch } = useTerminal();
+ const { conversationMap } = useConversations();
const rendersCount = useRendersCount();
console.warn('RENDER COUNT | CONVERSATION LIST | ', rendersCount);
@@ -69,8 +71,6 @@ export function ConversationList({ lookingForSomeone = false }: { lookingForSome
}
>
- {lookingForSomeone && }
-
{Object.values(conversationMap).map((currentConversation, index) => (
(null);
diff --git a/packages/desktop/src/components/MainPanel.tsx b/packages/desktop/src/components/MainPanel.tsx
index 933ccc2..8901a9b 100644
--- a/packages/desktop/src/components/MainPanel.tsx
+++ b/packages/desktop/src/components/MainPanel.tsx
@@ -1,15 +1,17 @@
-import { Grid, Container, Typography } from '@mui/material';
-import { blueGrey } from '@mui/material/colors';
+import { Container, Grid, Typography } from '@mui/material';
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 useTerminal from './Terminal';
export default function MainPanel() {
const { user } = useAuth();
- const { selectedConversation } = useTerminal();
+
+ const { selectedConversation } = useConversations();
// if selected conversation, show details
// do all fetching necessary to paint things here
diff --git a/packages/desktop/src/components/OmniSearchResults.tsx b/packages/desktop/src/components/OmniSearchResults.tsx
index 55b592a..617401b 100644
--- a/packages/desktop/src/components/OmniSearchResults.tsx
+++ b/packages/desktop/src/components/OmniSearchResults.tsx
@@ -16,6 +16,7 @@ import { ConversationRow } from './ConversationList';
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 useTerminal from './Terminal';
export default function OmniSearchResults({
@@ -25,7 +26,8 @@ export default function OmniSearchResults({
people: User[];
conversations: Conversation[];
}) {
- const { handleQuickDial, selectConversation } = useTerminal();
+ const { handleQuickDial } = useTerminal();
+ const { selectConversation } = useConversations();
return (
<>
diff --git a/packages/desktop/src/components/Terminal.tsx b/packages/desktop/src/components/Terminal.tsx
index cd0b2e3..ae0df62 100644
--- a/packages/desktop/src/components/Terminal.tsx
+++ b/packages/desktop/src/components/Terminal.tsx
@@ -26,22 +26,15 @@ import { blueGrey } from '@mui/material/colors';
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 { useSnackbar } from 'notistack';
import useStreamHandler from '../hooks/useStreamHandler';
interface ITerminalContext {
- conversationMap: ConversationMap;
- conversationContentMap: ConversationContentMap;
-
- selectedConversation?: Conversation;
- selectConversation?: (conversationId: string) => void;
-
handleQuickDial?: (otherUser: User) => void;
- getUser?: (userId: string) => Promise;
-
isUserSpeaking: boolean;
isCloudDoingMagic: boolean;
@@ -54,9 +47,6 @@ interface ITerminalContext {
}
const TerminalContext = React.createContext({
- conversationMap: {},
- conversationContentMap: {},
-
isUserSpeaking: false,
isCloudDoingMagic: false,
@@ -78,125 +68,11 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
const { enqueueSnackbar } = useSnackbar();
const { user, logout, nirvanaUser } = useAuth();
- const [selectedConversationId, setSelectedConversationId] = useState(undefined);
-
- const [conversationMap, updateConversationMap] = useImmer({});
- const [userMap, updateUserMap] = useImmer({});
- const [conversationContentMap, updateContentMap] = useImmer({});
+ const { conversationMap, conversationContentMap, selectConversation, selectedConversation } =
+ useConversations();
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
// not 100% consistent to the second, but still works...don't need atomicity
const handleQuickDial = useCallback(
@@ -219,33 +95,18 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
});
if (findExistingConversation) {
- setSelectedConversationId(findExistingConversation.id);
+ selectConversation(findExistingConversation.id);
return;
}
// create conversation in this case
const newConversationId = await createOneOnOneConversation(otherUser, nirvanaUser);
- setSelectedConversationId(newConversationId);
+ selectConversation(newConversationId);
} catch (error) {
enqueueSnackbar('Something went wrong, please try again', { variant: 'error' });
}
},
- [conversationMap, enqueueSnackbar, nirvanaUser],
- );
-
- 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],
+ [conversationMap, enqueueSnackbar, nirvanaUser, selectConversation],
);
const [isUserSpeaking, setIsUserSpeaking] = useState(false);
@@ -423,11 +284,6 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
[handleOmniSearch],
);
- const selectConversation = useCallback(
- (conversationId: string) => setSelectedConversationId(conversationId),
- [setSelectedConversationId],
- );
-
const [createConversationMode, setCreateConversationMode] = useState(false);
const handleStartConversation = useCallback(
@@ -453,29 +309,29 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
nirvanaUser,
conversationName ?? null,
);
- setSelectedConversationId(newConversationId);
+ selectConversation(newConversationId);
setCreateConversationMode(false);
} catch (error) {
enqueueSnackbar('Something went wrong, please try again', { variant: 'error' });
}
},
- [nirvanaUser, handleQuickDial, enqueueSnackbar, setCreateConversationMode],
+ [nirvanaUser, handleQuickDial, enqueueSnackbar, setCreateConversationMode, selectConversation],
);
const handleShowCreateConvoForm = useCallback(() => {
- setSelectedConversationId(undefined);
+ selectConversation(undefined);
setCreateConversationMode(true);
- }, [setCreateConversationMode, setSelectedConversationId]);
+ }, [setCreateConversationMode, selectConversation]);
const rendersCount = useRendersCount();
console.warn('RENDER COUNT | TERMINAL | ', rendersCount);
const handleEscape = useCallback(() => {
setCreateConversationMode(false);
- setSelectedConversationId(undefined);
+ selectConversation(undefined);
setSearchVal('');
- }, [setSelectedConversationId, setCreateConversationMode, setSearchVal]);
+ }, [selectConversation, setCreateConversationMode, setSearchVal]);
useKeyPressEvent(KeyboardShortcuts.escape.shortcutKey, handleEscape);
@@ -483,13 +339,8 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
) : (
-
+
)}
diff --git a/packages/desktop/src/providers/ConversationProvider.tsx b/packages/desktop/src/providers/ConversationProvider.tsx
index a6bffe2..97bd834 100644
--- a/packages/desktop/src/providers/ConversationProvider.tsx
+++ b/packages/desktop/src/providers/ConversationProvider.tsx
@@ -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 {
conversationMap: ConversationMap;
conversationContentMap: ConversationContentMap;
+
+ selectedConversation?: Conversation;
+ selectConversation?: (conversationId: string) => void;
+
+ getUser?: (userId: string) => Promise;
}
const ConversationContext = React.createContext({
conversationContentMap: {},
conversationMap: {},
});
+
+export function ConversationProvider({ children }: { children: React.ReactNode }) {
+ const { enqueueSnackbar } = useSnackbar();
+
+ const { user } = useAuth();
+
+ const [selectedConversationId, setSelectedConversationId] = useState(undefined);
+
+ const [conversationMap, updateConversationMap] = useImmer({});
+ const [userMap, updateUserMap] = useImmer({});
+ const [conversationContentMap, updateContentMap] = useImmer({});
+
+ 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 (
+
+ {children}
+
+ );
+}
+
+export default function useConversations() {
+ return useContext(ConversationContext);
+}