From cf5e16eeb9de5085b0e24553af37f81bf737797c Mon Sep 17 00:00:00 2001 From: talksik Date: Mon, 30 May 2022 08:08:36 -0500 Subject: [PATCH] nice flow and quick working without creating a new conversation --- .../core/src/models/conversation.model.ts | 9 +- .../src/components/NewConversationDialog.tsx | 157 ++++++++++++------ packages/desktop/src/components/Terminal.tsx | 61 +++++-- packages/desktop/src/firebase/firestore.ts | 84 ++++++---- 4 files changed, 208 insertions(+), 103 deletions(-) diff --git a/packages/core/src/models/conversation.model.ts b/packages/core/src/models/conversation.model.ts index 2e76e30..88324c3 100644 --- a/packages/core/src/models/conversation.model.ts +++ b/packages/core/src/models/conversation.model.ts @@ -1,4 +1,5 @@ import { FieldValue, serverTimestamp, Timestamp } from 'firebase/firestore'; +import { User } from './user.model'; export default class Conversation { id: string; @@ -9,6 +10,8 @@ export default class Conversation { public members: MemberMap, + public userCache: User[], + public name: string | null = null, public lastUpdatedDate = Timestamp.now(), @@ -20,8 +23,8 @@ export default class Conversation { } export type MemberMap = { - [memberId: string]: ConversationMember -} + [memberId: string]: ConversationMember; +}; export class ConversationMember { constructor( @@ -44,4 +47,4 @@ export enum MemberState { priority = 'priority', inbox = 'inbox', // deleted = "deleted" -} \ No newline at end of file +} diff --git a/packages/desktop/src/components/NewConversationDialog.tsx b/packages/desktop/src/components/NewConversationDialog.tsx index 2826e5c..2ad3d96 100644 --- a/packages/desktop/src/components/NewConversationDialog.tsx +++ b/packages/desktop/src/components/NewConversationDialog.tsx @@ -1,4 +1,4 @@ -import React, { HTMLAttributes, useCallback, useState } from 'react'; +import React, { HTMLAttributes, useCallback, useState, useEffect } from 'react'; import { Autocomplete, AutocompleteChangeReason, @@ -20,18 +20,21 @@ import { import { FiX } from 'react-icons/fi'; import { blueGrey } from '@mui/material/colors'; import { useSnackbar } from 'notistack'; -import { useDebounce } from 'react-use'; +import { useDebounce, useKeyPressEvent, useToggle } from 'react-use'; import { searchUsers } from '../firebase/firestore'; import { User } from '@nirvana/core/src/models/user.model'; import useAuth from '../providers/AuthProvider'; import UserDetailRow from '../subcomponents/UserDetailRow'; +import CircularProgress from '@mui/material/CircularProgress'; export default function NewConversationDialog({ open, handleClose, + handleSubmit, }: { open: boolean; handleClose: () => void; + handleSubmit: (selectedUsers: User[], conversationName?: string) => void; }) { const { enqueueSnackbar } = useSnackbar(); const { user } = useAuth(); @@ -75,16 +78,6 @@ export default function NewConversationDialog({ [setSearching, setSearchVal], ); - const handleSubmit = useCallback(() => { - try { - // - } catch (error) { - // - } - - handleClose(); - }, [handleClose]); - const renderOption = useCallback( (props: HTMLAttributes, option: User, state: AutocompleteRenderOptionState) => ( @@ -94,6 +87,48 @@ export default function NewConversationDialog({ [], ); + const [conversationName, setConversationName] = useState(''); + + useEffect(() => { + if (selectedUsers.length < 2) { + setConversationName(''); + } + }, [selectedUsers, setConversationName]); + + const handleChangeName = useCallback( + (e: React.ChangeEvent) => { + setConversationName(e.target.value); + }, + [setConversationName], + ); + + const [isSubmitting, toggleIsSubmitting] = useToggle(false); + + const handleSubmitLocal = useCallback(async () => { + if (selectedUsers.length === 0) { + enqueueSnackbar('Must a person!', { variant: 'error' }); + return; + } + + toggleIsSubmitting(); + + handleSubmit(selectedUsers, conversationName); + + // clear form for next time + setSelectedUsers([]); + setConversationName(''); + + toggleIsSubmitting(); + }, [ + selectedUsers, + setConversationName, + conversationName, + enqueueSnackbar, + handleSubmit, + setSelectedUsers, + toggleIsSubmitting, + ]); + return ( - - (typeof option === 'string' ? option : option.displayName)} - value={selectedUsers} - onChange={handleChangeSelections} - filterSelectedOptions - isOptionEqualToValue={(optionUser, valueUser) => optionUser.id === valueUser.id} - filterOptions={(options) => options} - inputValue={searchVal} - renderInput={(params) => ( + {isSubmitting ? ( + + ) : ( + + Start a Conversation + + + typeof option === 'string' ? option : option.displayName + } + value={selectedUsers} + onChange={handleChangeSelections} + filterSelectedOptions + isOptionEqualToValue={(optionUser, valueUser) => optionUser.id === valueUser.id} + filterOptions={(options) => options} + inputValue={searchVal} + renderInput={(params) => ( + + )} + /> + + {selectedUsers.length > 1 && ( )} - /> - - - - - - - + + + + + + )} ); diff --git a/packages/desktop/src/components/Terminal.tsx b/packages/desktop/src/components/Terminal.tsx index 919f041..68a4dcb 100644 --- a/packages/desktop/src/components/Terminal.tsx +++ b/packages/desktop/src/components/Terminal.tsx @@ -50,6 +50,7 @@ import Navbar from './Navbar'; import MainPanel from './MainPanel'; import { ConversationList } from './ConversationList'; import NewConversationDialog from './NewConversationDialog'; +import { createGroupConversation } from '../firebase/firestore'; type ConversationMap = { [conversationId: string]: Conversation; }; @@ -69,7 +70,7 @@ interface ITerminalContext { selectedConversation?: Conversation; selectConversation?: (conversationId: string) => void; - handleQuickDial?: (otherUserId: string) => void; + handleQuickDial?: (otherUser: User) => void; getUser?: (userId: string) => Promise; @@ -159,7 +160,7 @@ const handleOnStartRecording = (e: BlobEvent) => { // todo: extract each use effect to custom hook and will be clean export function TerminalProvider({ children }: { children?: React.ReactNode }) { const { enqueueSnackbar } = useSnackbar(); - const { user, logout } = useAuth(); + const { user, logout, nirvanaUser } = useAuth(); const [selectedConversationId, setSelectedConversationId] = useState(undefined); @@ -282,7 +283,7 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { // handle create or open existing conversation // not 100% consistent to the second, but still works...don't need atomicity const handleQuickDial = useCallback( - async (otherUserId: string) => { + async (otherUser: User) => { setSearchVal(''); try { @@ -292,8 +293,8 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { const findExistingConversation = Object.values(conversationMap).find((convo) => { if ( convo.memberIdsList?.length === 2 && - convo.memberIdsList.includes(user.uid) && - convo.memberIdsList.includes(otherUserId) + convo.memberIdsList.includes(nirvanaUser.id) && + convo.memberIdsList.includes(otherUser.id) ) { return true; } @@ -307,14 +308,14 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { } // create conversation in this case - const newConversationId = await createOneOnOneConversation(otherUserId, user.uid); + const newConversationId = await createOneOnOneConversation(otherUser, nirvanaUser); enqueueSnackbar('started conversation!', { variant: 'success' }); setSelectedConversationId(newConversationId); } catch (error) { enqueueSnackbar('Something went wrong, please try again', { variant: 'error' }); } }, - [conversationMap, enqueueSnackbar, user], + [conversationMap, enqueueSnackbar, nirvanaUser], ); const getUser = useCallback( @@ -496,12 +497,42 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { setAnchorEl(null); }; - // const handleCreateConversation = useCallback(() => { - - // }, []) - const [createConversationMode, setCreateConversationMode] = useState(true); + const handleStartConversation = useCallback( + async (selectedUsers: User[], conversationName?: string) => { + if (selectedUsers.length === 0) { + enqueueSnackbar('Must select more than one person'); + return; + } + + try { + // one on one, handle quick dial to prevent another conversation + if (selectedUsers.length === 1) { + await handleQuickDial(selectedUsers[0]); + + setCreateConversationMode(false); + return; + } + + // create group chat + // create conversation in this case + const newConversationId = await createGroupConversation( + selectedUsers, + nirvanaUser, + conversationName ?? null, + ); + enqueueSnackbar('started group conversation!', { variant: 'success' }); + setSelectedConversationId(newConversationId); + + setCreateConversationMode(false); + } catch (error) { + enqueueSnackbar('Something went wrong, please try again', { variant: 'error' }); + } + }, + [nirvanaUser, handleQuickDial, enqueueSnackbar, setCreateConversationMode], + ); + const handleShowCreateConvoForm = useCallback(() => { setSelectedConversationId(undefined); setCreateConversationMode(true); @@ -693,7 +724,11 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { {children} {/* create chat dialog */} - + ); } @@ -739,7 +774,7 @@ function ListPeople({ people }: { people: User[] }) { {people.map((person) => ( - handleQuickDial(person.uid)}> + handleQuickDial(person)}> diff --git a/packages/desktop/src/firebase/firestore.ts b/packages/desktop/src/firebase/firestore.ts index a6762f0..3f416df 100644 --- a/packages/desktop/src/firebase/firestore.ts +++ b/packages/desktop/src/firebase/firestore.ts @@ -160,19 +160,23 @@ export const getConversationContentQueryLIVE = (conversationId: string) => * @returns id of new conversation */ export const createOneOnOneConversation = async ( - otherUserId: string, - myUserId: string, -): Promise => { - const myMember = new ConversationMember(myUserId, MemberRole.admin, MemberState.inbox); - const otherMember = new ConversationMember(otherUserId, MemberRole.regular, MemberState.inbox); - - const newMemberMap: MemberMap = { - [myUserId]: { ...myMember }, - [otherUserId]: { ...otherMember }, - }; - const newConversation = new Conversation(myUserId, [myUserId, otherUserId], newMemberMap); - + otherUser: User, + currentUser: User, +): Promise => { try { + const myMember = new ConversationMember(currentUser.id, MemberRole.admin, MemberState.inbox); + const otherMember = new ConversationMember(otherUser.id, MemberRole.regular, MemberState.inbox); + + const newMemberMap: MemberMap = { + [currentUser.id]: { ...myMember }, + [otherUser.id]: { ...otherMember }, + }; + + const userCache: User[] = [{ ...currentUser }, { ...otherUser }]; + const membersList = [currentUser.id, otherUser.id]; + + const newConversation = new Conversation(currentUser.id, membersList, newMemberMap, userCache); + const newDoc = await addDoc(db.conversations, newConversation); return newDoc.id; } catch (e) { @@ -183,28 +187,42 @@ export const createOneOnOneConversation = async ( }; export const createGroupConversation = async ( - otherUserIds: string[], - myUserId: string, -): Promise => { - const myMember = new ConversationMember(myUserId, MemberRole.admin, MemberState.inbox); - - const newMemberMap: MemberMap = { - [myUserId]: { ...myMember }, - }; - - otherUserIds.forEach((otherMemberId) => { - const otherMember = new ConversationMember( - otherMemberId, - MemberRole.regular, - MemberState.inbox, - ); - newMemberMap[otherMemberId] = { ...otherMember }; - }); - - const newConversation = new Conversation(myUserId, [myUserId, ...otherUserIds], newMemberMap); - + otherUsers: User[], + adminUser: User, + conversationName: string | null, +): Promise => { try { - await addDoc(db.conversations, newConversation); + const userCache = [{ ...adminUser }]; + + const myMember = new ConversationMember(adminUser.id, MemberRole.admin, MemberState.inbox); + + const newMemberMap: MemberMap = { + [adminUser.id]: { ...myMember }, + }; + + otherUsers.forEach((otherUser) => { + const otherMember = new ConversationMember( + otherUser.id, + MemberRole.regular, + MemberState.inbox, + ); + + newMemberMap[otherUser.id] = { ...otherMember }; + + userCache.push({ ...otherUser }); + }); + + const membersList = [adminUser.id, ...otherUsers.map((otherUser) => otherUser.id)]; + const newConversation = new Conversation( + adminUser.id, + membersList, + newMemberMap, + userCache, + conversationName, + ); + + const newConversationDoc = await addDoc(db.conversations, newConversation); + return newConversationDoc.id; } catch (e) { console.error('Error : ', e);