diff --git a/packages/web/components/Audio/AudioHandler.tsx b/packages/web/components/Audio/AudioHandler.tsx index 552cff1..d0ded6d 100644 --- a/packages/web/components/Audio/AudioHandler.tsx +++ b/packages/web/components/Audio/AudioHandler.tsx @@ -2,16 +2,20 @@ import { useEffect, useState } from "react"; import { configure, GlobalHotKeys, KeyMap } from "react-hotkeys"; import { useRecoilState, useRecoilValue } from "recoil"; import { + allRelevantConversationsAtom, + audioQueueAtom, hasMicPermissionsAtom, isRecordingAtom, priorityConvosSelector, - selectedPriorityConvoAtom, + selectedConvoAtom, } from "../../recoil/main"; import MicRecorder from "mic-recorder-to-mp3"; import toast from "react-hot-toast"; import { v4 as uuidv4 } from "uuid"; import { conversationService } from "@nirvana/common/services"; import { useAuth } from "../../contexts/authContext"; +import { useRouter } from "next/router"; +import { AudioClip } from "@nirvana/common/models/conversation"; // New instance const recorder = new MicRecorder({ @@ -22,72 +26,48 @@ const MAX_SHORTCUT_MAPPINGS_PRIORITY = 8; export default function AudioHandler() { const { currUser } = useAuth(); + const router = useRouter(); + + const { convoId } = router.query; const priorityConvos = useRecoilValue(priorityConvosSelector); + const mapAllConvos = useRecoilValue(allRelevantConversationsAtom); + const [mapShortcutsToConvoId, setmapShortcutsToConvoId] = useState< Map >(new Map()); - const [selectedConvoId, setSelectedConversationId] = useRecoilState( - selectedPriorityConvoAtom - ); + const [selectedConvoId, setSelectedConversationId] = + useRecoilState(selectedConvoAtom); + + const [audioQueue, setAudioQueue] = useRecoilState(audioQueueAtom); + + function onEndedPlaying(e) { + toast.success("finished playing"); + + // remove from queue and the queue manager will handle the rest + setAudioQueue((prevQueue) => { + const newQueue: AudioClip[] = [...prevQueue]; + newQueue.shift(); + return newQueue; + }); + } + + useEffect(() => { + if (audioQueue.length > 0) { + const audio = new Audio(audioQueue[0].audioDataUrl); + audio.onended = onEndedPlaying; + audio.play(); + } + }, [audioQueue]); const [hasMicPermissions, sethasMicPermissions] = useRecoilState( hasMicPermissionsAtom ); - // set keyboard shortcuts for 1->8, but only if this component is shown, and not in stuff like - // convo view - useEffect(() => { - // reset entire map of shortcuts now with new list of priority convos - - const newMap = new Map(); - - priorityConvos.map((convo, i) => { - if (i < MAX_SHORTCUT_MAPPINGS_PRIORITY) { - const shortcut = (i + 1).toString(); - newMap.set(shortcut, convo.id); - } - }); - setmapShortcutsToConvoId(newMap); - }, [priorityConvos]); - - const selectingPriorityConvoHandler = (event) => { - const key = event.key; - - // select the convo based on the right convo - - if (!mapShortcutsToConvoId.has(key)) { - toast.error("Not a valid conversation selected"); - return; - } - - const selectedConvoId = mapShortcutsToConvoId.get(key); - - if (selectedConvoId) { - setSelectedConversationId(selectedConvoId); - } - }; - - // set keyboard shortcuts for 1->8, but only if this component is shown, and not in stuff like - // convo view - useEffect(() => { - // reset entire map of shortcuts now with new list of priority convos - - const newMap = new Map(); - - priorityConvos.map((convo, i) => { - if (i < MAX_SHORTCUT_MAPPINGS_PRIORITY) { - const shortcut = (i + 1).toString(); - newMap.set(shortcut, convo.id); - } - }); - setmapShortcutsToConvoId(newMap); - }, [priorityConvos]); - - // checking for permissions for recording useEffect(() => { try { + // checking for permissions for recording // won't work in https!!! navigator.mediaDevices .getUserMedia({ audio: true }) @@ -111,6 +91,22 @@ export default function AudioHandler() { } }, []); + // set keyboard shortcuts for 1->8, but only if this component is shown, and not in stuff like + // convo view + useEffect(() => { + // reset entire map of shortcuts now with new list of priority convos + + const newMap = new Map(); + + priorityConvos.map((convo, i) => { + if (i < MAX_SHORTCUT_MAPPINGS_PRIORITY) { + const shortcut = (i + 1).toString(); + newMap.set(shortcut, convo.id); + } + }); + setmapShortcutsToConvoId(newMap); + }, [priorityConvos]); + configure({ ignoreTags: ["input", "select", "textarea"], }); @@ -205,9 +201,7 @@ export default function AudioHandler() { return; } - const currConvo = priorityConvos.find( - (convo) => convo.id == selectedConvoId - ); + const currConvo = mapAllConvos.get(selectedConvoId); if (!currConvo?.cachedAudioClip) { toast.error("nothing to play"); @@ -218,6 +212,29 @@ export default function AudioHandler() { audio.play(); }; + const selectingPriorityConvoHandler = (event) => { + const key = event.key; + + // STOP if we are in a convo detail page, as we want to talk in this channel, if so + if (convoId) { + toast.error("Cannot select a priority convo from here!"); + return; + } + + // select the convo based on the shortcut + + if (!mapShortcutsToConvoId.has(key)) { + toast.error("Not a valid conversation selected"); + return; + } + + const selectedConvoId = mapShortcutsToConvoId.get(key); + + if (selectedConvoId) { + setSelectedConversationId(selectedConvoId); + } + }; + const keyMap: KeyMap = { SELECT_PRIORITY_CONVO: ["1", "2", "3", "4", "5", "6", "7", "8"], START_RECORDING: { diff --git a/packages/web/components/Conversations/PriorityConvoRow.tsx b/packages/web/components/Conversations/PriorityConvoRow.tsx index 4df1fa0..9ee0974 100644 --- a/packages/web/components/Conversations/PriorityConvoRow.tsx +++ b/packages/web/components/Conversations/PriorityConvoRow.tsx @@ -2,7 +2,7 @@ import { Avatar, Tooltip } from "antd"; import { useRecoilState, useRecoilValue } from "recoil"; import { checkIncomingMessageSelector, - selectedPriorityConvoAtom, + selectedConvoAtom, } from "../../recoil/main"; import Conversation from "../../../common/models/conversation"; import { MasterAvatarGroupWithUserFetch } from "../UserDetails/MasterAvatarGroup"; @@ -23,9 +23,7 @@ export default function PriorityConvoRow(props: { itemIndex: number; }) { // figure out if this is a selectedConvo - const [selectedConvo, setSelectedConvo] = useRecoilState( - selectedPriorityConvoAtom - ); + const [selectedConvo, setSelectedConvo] = useRecoilState(selectedConvoAtom); const isSelected = selectedConvo == props.conversation.id ? true : false; diff --git a/packages/web/components/Conversations/TimelineAudioClip.tsx b/packages/web/components/Conversations/TimelineAudioClip.tsx index 7113a2c..538a51f 100644 --- a/packages/web/components/Conversations/TimelineAudioClip.tsx +++ b/packages/web/components/Conversations/TimelineAudioClip.tsx @@ -2,10 +2,13 @@ import { yesterday } from "@nirvana/common/helpers/dateTime"; import { AudioClip } from "@nirvana/common/models/conversation"; import moment from "moment"; import toast from "react-hot-toast"; -import { useRecoilValue } from "recoil"; +import { useRecoilState, useRecoilValue } from "recoil"; import { allRelevantContactsAtom, allUsersConversationsAtom, + audioQueueAtom, + audioQueueCurrentClipSelector, + selectedConvoAtom, } from "../../recoil/main"; import { MasterAvatarGroupWithUserFetch } from "../UserDetails/MasterAvatarGroup"; import { useState } from "react"; @@ -21,6 +24,9 @@ export default function TimelineAudioClip(props: { const audioClipUser = relContactsMap.get(props.audioClip.senderUserId); const userConvoAssoc = userConvosMap.get(props.convoId); + const audioQueueCurrentClip = useRecoilValue(audioQueueCurrentClipSelector); + const [audioQueue, setAudioQueue] = useRecoilState(audioQueueAtom); + const [audioDuration, setAudioDuration] = useState(); const loadedMetadata = (data) => { @@ -33,11 +39,21 @@ export default function TimelineAudioClip(props: { // audio.onloadedmetadata = loadedMetadata; const playAudioClip = () => { - toast(`${audioClipUser?.firstName} speaking...`); - - audio.play(); + setAudioQueue((prevQueue) => { + return [...prevQueue, props.audioClip]; + }); }; + let customClasses = "bg-slate-100 border"; + if (audioQueueCurrentClip?.id == props.audioClip.id) { + customClasses = "animate-pulse bg-orange-200"; + } else if ( + props.audioClip.createdDate < + (userConvoAssoc?.lastInteractionDate || yesterday) + ) { + customClasses = "bg-sky-100"; + } + return ( { console.log("woah"); diff --git a/packages/web/components/MainTabsOrPages/ViewConvo.tsx b/packages/web/components/MainTabsOrPages/ViewConvo.tsx index 4f3ed4b..4bddae2 100644 --- a/packages/web/components/MainTabsOrPages/ViewConvo.tsx +++ b/packages/web/components/MainTabsOrPages/ViewConvo.tsx @@ -18,9 +18,10 @@ import { } from "react-icons/fa"; import LinkIcon from "../Drawer/LinkIcon"; import UserAvatar, { UserAvatarSizes } from "../UserDetails/UserAvatar"; -import { useRecoilValue } from "recoil"; +import { useRecoilValue, useSetRecoilState } from "recoil"; import { allRelevantConversationsAtom, + selectedConvoAtom, userConvoAssociationSelector, } from "../../recoil/main"; import { useRouter } from "next/router"; @@ -73,62 +74,6 @@ const testDrawerItems: { }, ]; -const testAudioClips: { - senderName: string; - relativeSentTime: string; - duration: number; - alreadyPlayed: boolean; - isGap?: boolean; - isLink?: boolean; -}[] = [ - { - senderName: "John", - relativeSentTime: "yesterday", - duration: 150, - alreadyPlayed: true, - }, - - { - senderName: "Moha", - relativeSentTime: "5 minutes ago", - duration: 600, - alreadyPlayed: false, - isGap: true, - }, - { - senderName: "Alex", - relativeSentTime: "7 hours ago", - duration: 10, - alreadyPlayed: false, - }, - { - senderName: "Ben", - relativeSentTime: "2 hours ago", - duration: 300, - alreadyPlayed: false, - }, - - { - senderName: "Alex", - relativeSentTime: "30 minutes ago", - duration: 200, - alreadyPlayed: false, - }, - { - senderName: "Moha", - relativeSentTime: "8 minutes ago", - duration: 600, - alreadyPlayed: false, - isLink: true, - }, - { - senderName: "Moha", - relativeSentTime: "5 minutes ago", - duration: 600, - alreadyPlayed: false, - }, -]; - const AUDIO_CLIP_FETCH_LIMIT = 50; export default function ViewConvo(props: { conversationId: string }) { @@ -158,7 +103,10 @@ export default function ViewConvo(props: { conversationId: string }) { const [audioClips, setAudioClips] = useState([] as AudioClip[]); + const setSelectedConvoId = useSetRecoilState(selectedConvoAtom); + useEffect(() => { + // should have this convo, otherwise unauthorized if (!convo || !convo.activeMembers.includes(currUser!.uid)) { toast.error("Not authorized for this conversation"); @@ -170,6 +118,9 @@ export default function ViewConvo(props: { conversationId: string }) { return; } + // set the selected convo as this one as we are now on this page + setSelectedConvoId(props.conversationId); + // subscribe to the data for the conversation such that we can render the timeline // get all audioClips for this conversation realtime listener so that when I speak it's also put on the timeline // any new message is seamlessly added to the timeline @@ -235,22 +186,21 @@ export default function ViewConvo(props: { conversationId: string }) { {convo?.name} - - - + {convo?.activeMembers && ( + + )} + {convo?.activeMembers.length} members @@ -258,6 +208,12 @@ export default function ViewConvo(props: { conversationId: string }) { + {userConvoAssoc?.state && ( + + )} + {userConvoAssoc?.state != ConversationMemberState.default && (