refactor to a high level audio handler

This commit is contained in:
Arjun Patel
2022-02-06 12:28:30 -08:00
parent 8ab20e7df8
commit 1b84b4324a
4 changed files with 269 additions and 237 deletions
@@ -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<string, string>
>(new Map<string, string>());
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<string, string>();
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<string, string>();
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<string>("");
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 (
<GlobalHotKeys keyMap={keyMap} handlers={handlers} allowChanges={true} />
);
}
@@ -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 (
<span
onClick={handleViewConversationDetails}
@@ -99,7 +109,13 @@ export default function ConversationFullRow(props: {
group-hover:visible invisible absolute right-5 text-slate-400"
>
<Tooltip title={"Play last convo chunk."}>
<span className="p-2 rounded-full hover:cursor-pointer hover:bg-slate-200">
<span
onClick={(e) => {
e.stopPropagation();
playLastClip();
}}
className="p-2 rounded-full hover:cursor-pointer hover:bg-slate-200"
>
<FaPlay className="ml-auto text-lg text-emerald-500" />
</span>
</Tooltip>
@@ -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<string, string>
>(new Map<string, string>());
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<string, string>();
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<string>("");
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 ? (
<>
<GlobalHotKeys
keyMap={keyMap}
handlers={handlers}
allowChanges={true}
/>
{priorityConvos.map((convo, i) => (
<PriorityConvoRow
key={convo.id}
+3
View File
@@ -15,6 +15,7 @@ import { QueryRoutes, Routes } from "@nirvana/common/helpers/routes";
import ViewConvo from "../../components/MainTabsOrPages/ViewConvo";
import MainRecoilDataHandler from "../../recoil/MainRecoilDataHandler";
import { useAuth } from "../../contexts/authContext";
import AudioHandler from "../../components/Audio/AudioHandler";
export default function Me() {
// TODO: if not authenticated, take user away
@@ -129,6 +130,8 @@ Me.getLayout = function (content: ReactElement) {
<KeyboardShortcutHandler />
<AudioHandler />
<Header />
{content}