From 1b84b4324abc4d248effa69711bc58da042dd56e Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Sun, 6 Feb 2022 12:28:30 -0800 Subject: [PATCH] refactor to a high level audio handler --- .../web/components/Audio/AudioHandler.tsx | 246 ++++++++++++++++++ .../Conversations/ConversationFullRow.tsx | 18 +- .../components/MainTabsOrPages/Priority.tsx | 239 +---------------- packages/web/pages/s/index.tsx | 3 + 4 files changed, 269 insertions(+), 237 deletions(-) create mode 100644 packages/web/components/Audio/AudioHandler.tsx diff --git a/packages/web/components/Audio/AudioHandler.tsx b/packages/web/components/Audio/AudioHandler.tsx new file mode 100644 index 0000000..552cff1 --- /dev/null +++ b/packages/web/components/Audio/AudioHandler.tsx @@ -0,0 +1,246 @@ +import { useEffect, useState } from "react"; +import { configure, GlobalHotKeys, KeyMap } from "react-hotkeys"; +import { useRecoilState, useRecoilValue } from "recoil"; +import { + hasMicPermissionsAtom, + isRecordingAtom, + priorityConvosSelector, + selectedPriorityConvoAtom, +} 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"; + +// New instance +const recorder = new MicRecorder({ + bitRate: 128, +}); + +const MAX_SHORTCUT_MAPPINGS_PRIORITY = 8; + +export default function AudioHandler() { + const { currUser } = useAuth(); + + const priorityConvos = useRecoilValue(priorityConvosSelector); + const [mapShortcutsToConvoId, setmapShortcutsToConvoId] = useState< + Map + >(new Map()); + + const [selectedConvoId, setSelectedConversationId] = useRecoilState( + selectedPriorityConvoAtom + ); + + 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 { + // won't work in https!!! + navigator.mediaDevices + .getUserMedia({ audio: true }) + .then((stream) => { + // stop playing anything + // stopBothVideoAndAudio(stream); + + console.log("Permission Granted"); + sethasMicPermissions(true); + }) + .catch((e) => { + console.log("Permission Denied"); + toast.error( + "Please make sure that you have connected a microphone and given permissions." + ); + sethasMicPermissions(false); + }); + } catch (error) { + console.log(error); + toast.error("something went wrong"); + } + }, []); + + configure({ + ignoreTags: ["input", "select", "textarea"], + }); + + const [recordingToastId, setRecordingToastId] = useState(""); + const [isRecording, setIsRecording] = useRecoilState(isRecordingAtom); + + const startRecording = () => { + // check if there is a person selected + if (!selectedConvoId) { + toast.error("You must select a person first!"); + return; + } + + // check that we have mic permissions + if (!hasMicPermissions) { + toast.error( + "Please make sure that you have connected a microphone and given permissions." + ); + + return; + } + + // check that we are not in flow state + + // start recording + recorder + .start() + .then(() => { + // toast.success("started recording"); + console.log("recording started"); + + setIsRecording(true); + }) + .catch((e) => + toast.error("there was a problem in starting your recording") + ); + + const toastId = toast.loading("speaking to Patels"); + setRecordingToastId(toastId); + }; + + const stopRecording = () => { + console.log("stop recording"); + toast.remove(recordingToastId); + + if (!isRecording) { + return; + } + + setIsRecording(false); + + if (!selectedConvoId) { + toast.error("problem in sending clip"); + return; + } + + recorder + .stop() + .getMp3() + .then(async ([buffer, blob]) => { + const blobURL = URL.createObjectURL(blob); + + const file = new File(buffer, uuidv4() + ".mp3", { + type: blob.type, + lastModified: Date.now(), + }); + + const player = new Audio(URL.createObjectURL(file)); + // player.onended = onEndedPlaying; + player.play(); + + // send to service which handles the upload to blob and also the conversation database + await conversationService.sendAudioClip( + currUser!.uid, + file, + selectedConvoId + ); + + console.log("successfully sent audio clip"); + toast.success("successfully sent audio clip"); + }) + .catch((error) => { + console.log(error); + toast.error("problem in sending audio clip"); + }); + }; + + const playLastClip = () => { + if (!selectedConvoId) { + toast.error("select a conversation before playing"); + return; + } + + const currConvo = priorityConvos.find( + (convo) => convo.id == selectedConvoId + ); + + if (!currConvo?.cachedAudioClip) { + toast.error("nothing to play"); + return; + } + + const audio = new Audio(currConvo?.cachedAudioClip.audioDataUrl); + audio.play(); + }; + + const keyMap: KeyMap = { + SELECT_PRIORITY_CONVO: ["1", "2", "3", "4", "5", "6", "7", "8"], + START_RECORDING: { + name: "START RECORDING", + sequence: "r", + action: "keydown", + }, + STOP_RECORDING: { + name: "STOP RECORDING", + sequence: "r", + action: "keyup", + }, + PLAY_LAST_CLIP: "space", + }; + + const handlers = { + SELECT_PRIORITY_CONVO: selectingPriorityConvoHandler, + START_RECORDING: startRecording, + STOP_RECORDING: stopRecording, + PLAY_LAST_CLIP: playLastClip, + }; + + return ( + + ); +} diff --git a/packages/web/components/Conversations/ConversationFullRow.tsx b/packages/web/components/Conversations/ConversationFullRow.tsx index 33c55d4..45a094b 100644 --- a/packages/web/components/Conversations/ConversationFullRow.tsx +++ b/packages/web/components/Conversations/ConversationFullRow.tsx @@ -62,6 +62,16 @@ export default function ConversationFullRow(props: { }); }; + const playLastClip = () => { + if (!props.conversation.cachedAudioClip) { + toast.error("Nothing to play"); + return; + } + 3; + const audio = new Audio(props.conversation.cachedAudioClip.audioDataUrl); + audio.play(); + }; + return ( - + { + e.stopPropagation(); + playLastClip(); + }} + className="p-2 rounded-full hover:cursor-pointer hover:bg-slate-200" + > diff --git a/packages/web/components/MainTabsOrPages/Priority.tsx b/packages/web/components/MainTabsOrPages/Priority.tsx index 8dd8e53..8a7c90d 100644 --- a/packages/web/components/MainTabsOrPages/Priority.tsx +++ b/packages/web/components/MainTabsOrPages/Priority.tsx @@ -1,250 +1,17 @@ -import { Avatar, Tooltip, Badge } from "antd"; import React from "react"; -import { useRecoilValue, useSetRecoilState, useRecoilState } from "recoil"; -import { - FaCircle, - FaMicrophone, - FaPlay, - FaRocket, - FaUser, -} from "react-icons/fa"; -import { - hasMicPermissionsAtom, - isRecordingAtom, - priorityConvosSelector, - selectedPriorityConvoAtom, -} from "../../recoil/main"; +import { useRecoilValue } from "recoil"; +import { FaRocket } from "react-icons/fa"; +import { priorityConvosSelector } from "../../recoil/main"; import PriorityConvoRow from "../Conversations/PriorityConvoRow"; -import { configure, GlobalHotKeys, HotKeys, KeyMap } from "react-hotkeys"; -import { useEffect, useState } from "react"; -import toast from "react-hot-toast"; -import MicRecorder from "mic-recorder-to-mp3"; -import { v4 as uuidv4 } from "uuid"; -import { conversationService } from "../../../common/services/index"; -import { AudioClip } from "@nirvana/common/models/conversation"; import { useAuth } from "../../contexts/authContext"; -// New instance -const recorder = new MicRecorder({ - bitRate: 128, -}); - -const MAX_SHORTCUT_MAPPINGS_PRIORITY = 8; - export default function Priority() { - const { currUser } = useAuth(); - const priorityConvos = useRecoilValue(priorityConvosSelector); - const [mapShortcutsToConvoId, setmapShortcutsToConvoId] = useState< - Map - >(new Map()); - - const [selectedConvoId, setSelectedConversationId] = useRecoilState( - selectedPriorityConvoAtom - ); - - 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]); - - // checking for permissions for recording - useEffect(() => { - try { - // won't work in https!!! - navigator.mediaDevices - .getUserMedia({ audio: true }) - .then((stream) => { - // stop playing anything - // stopBothVideoAndAudio(stream); - - console.log("Permission Granted"); - sethasMicPermissions(true); - }) - .catch((e) => { - console.log("Permission Denied"); - toast.error( - "Please make sure that you have connected a microphone and given permissions." - ); - sethasMicPermissions(false); - }); - } catch (error) { - console.log(error); - toast.error("something went wrong"); - } - }, []); - - 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); - } - }; - - configure({ - ignoreTags: ["input", "select", "textarea"], - }); - - const [recordingToastId, setRecordingToastId] = useState(""); - const [isRecording, setIsRecording] = useRecoilState(isRecordingAtom); - - const startRecording = () => { - // check if there is a person selected - if (!selectedConvoId) { - toast.error("You must select a person first!"); - return; - } - - // check that we have mic permissions - if (!hasMicPermissions) { - toast.error( - "Please make sure that you have connected a microphone and given permissions." - ); - - return; - } - - // check that we are not in flow state - - // start recording - recorder - .start() - .then(() => { - // toast.success("started recording"); - console.log("recording started"); - - setIsRecording(true); - }) - .catch((e) => - toast.error("there was a problem in starting your recording") - ); - - const toastId = toast.loading("speaking to Patels"); - setRecordingToastId(toastId); - }; - - const stopRecording = () => { - console.log("stop recording"); - toast.remove(recordingToastId); - - if (!isRecording) { - return; - } - - setIsRecording(false); - - if (!selectedConvoId) { - toast.error("problem in sending clip"); - return; - } - - recorder - .stop() - .getMp3() - .then(async ([buffer, blob]) => { - const blobURL = URL.createObjectURL(blob); - - const file = new File(buffer, uuidv4() + ".mp3", { - type: blob.type, - lastModified: Date.now(), - }); - - const player = new Audio(URL.createObjectURL(file)); - // player.onended = onEndedPlaying; - player.play(); - - // send to service which handles the upload to blob and also the conversation database - await conversationService.sendAudioClip( - currUser!.uid, - file, - selectedConvoId - ); - - console.log("successfully sent audio clip"); - toast.success("successfully sent audio clip"); - }) - .catch((error) => { - console.log(error); - toast.error("problem in sending audio clip"); - }); - }; - - const playLastClip = () => { - if (!selectedConvoId) { - toast.error("select a conversation before playing"); - return; - } - - const currConvo = priorityConvos.find( - (convo) => convo.id == selectedConvoId - ); - - if (!currConvo?.cachedAudioClip) { - toast.error("nothing to play"); - return; - } - - const audio = new Audio(currConvo?.cachedAudioClip.audioDataUrl); - audio.play(); - }; - - const keyMap: KeyMap = { - SELECT_PRIORITY_CONVO: ["1", "2", "3", "4", "5", "6", "7", "8"], - START_RECORDING: { - name: "START RECORDING", - sequence: "r", - action: "keydown", - }, - STOP_RECORDING: { - name: "STOP RECORDING", - sequence: "r", - action: "keyup", - }, - PLAY_LAST_CLIP: "space", - }; - - const handlers = { - SELECT_PRIORITY_CONVO: selectingPriorityConvoHandler, - START_RECORDING: startRecording, - STOP_RECORDING: stopRecording, - PLAY_LAST_CLIP: playLastClip, - }; return ( <> {priorityConvos?.length > 0 ? ( <> - - {priorityConvos.map((convo, i) => ( + +
{content}