cleaning up use of conversations and much nicer!

This commit is contained in:
talksik
2022-05-31 06:47:01 -05:00
parent ad1a58ab5c
commit e39df85faa
8 changed files with 217 additions and 191 deletions
+8 -5
View File
@@ -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(
<ErrorParent>
<ElectronProvider>
<AuthProvider>
<ZenProvider>
<TerminalProvider>
<></>
</TerminalProvider>
</ZenProvider>
<ConversationProvider>
<ZenProvider>
<TerminalProvider>
<></>
</TerminalProvider>
</ZenProvider>
</ConversationProvider>
</AuthProvider>
</ElectronProvider>
</ErrorParent>
@@ -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);
@@ -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
</ListSubheader>
}
>
{lookingForSomeone && <CircularProgress />}
{Object.values(conversationMap).map((currentConversation, index) => (
<ConversationRow
key={`${currentConversation.id}-priorityConvoList`}
@@ -110,7 +110,8 @@ export function ConversationRow({
index?: number;
}) {
const { user } = useAuth();
const { getUser, selectedConversation, selectConversation } = useTerminal();
const { selectedConversation, selectConversation } = useConversations();
const rendersCount = useRendersCount();
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 { blueGrey } from '@mui/material/colors';
import useAuth from '../providers/AuthProvider';
import useConversations from '../providers/ConversationProvider';
import useTerminal from './Terminal';
export default function FooterControls() {
const { selectedConversation, handleOmniSearch } = useTerminal();
const { handleOmniSearch } = useTerminal();
const { selectedConversation } = useConversations();
const { user, logout } = useAuth();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
@@ -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
@@ -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 (
<>
+13 -164
View File
@@ -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<User | undefined>;
isUserSpeaking: boolean;
isCloudDoingMagic: boolean;
@@ -54,9 +47,6 @@ interface ITerminalContext {
}
const TerminalContext = React.createContext<ITerminalContext>({
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<string>(undefined);
const [conversationMap, updateConversationMap] = useImmer<ConversationMap>({});
const [userMap, updateUserMap] = useImmer<UserMap>({});
const [conversationContentMap, updateContentMap] = useImmer<ConversationContentMap>({});
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<boolean>(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<boolean>(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 }) {
<TerminalContext.Provider
value={{
isUserSpeaking,
selectedConversation,
conversationMap,
getUser,
handleQuickDial,
selectConversation,
isCloudDoingMagic,
conversationContentMap,
createConversationMode,
handleShowCreateConvoForm,
handleEscape,
@@ -525,9 +376,7 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
conversations={searchConversationsResults}
/>
) : (
<ConversationList
lookingForSomeone={selectedConversationId && !selectedConversation}
/>
<ConversationList />
)}
</Box>
</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 {
conversationMap: ConversationMap;
conversationContentMap: ConversationContentMap;
selectedConversation?: Conversation;
selectConversation?: (conversationId: string) => void;
getUser?: (userId: string) => Promise<User | undefined>;
}
const ConversationContext = React.createContext<IConversationContext>({
conversationContentMap: {},
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);
}