getting a basic queue but need to add whole timeline nicely

This commit is contained in:
Arjun Patel
2022-02-06 13:21:51 -08:00
parent 1b84b4324a
commit 19a706fd93
7 changed files with 147 additions and 141 deletions
+74 -57
View File
@@ -2,16 +2,20 @@ import { useEffect, useState } from "react";
import { configure, GlobalHotKeys, KeyMap } from "react-hotkeys"; import { configure, GlobalHotKeys, KeyMap } from "react-hotkeys";
import { useRecoilState, useRecoilValue } from "recoil"; import { useRecoilState, useRecoilValue } from "recoil";
import { import {
allRelevantConversationsAtom,
audioQueueAtom,
hasMicPermissionsAtom, hasMicPermissionsAtom,
isRecordingAtom, isRecordingAtom,
priorityConvosSelector, priorityConvosSelector,
selectedPriorityConvoAtom, selectedConvoAtom,
} from "../../recoil/main"; } from "../../recoil/main";
import MicRecorder from "mic-recorder-to-mp3"; import MicRecorder from "mic-recorder-to-mp3";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { conversationService } from "@nirvana/common/services"; import { conversationService } from "@nirvana/common/services";
import { useAuth } from "../../contexts/authContext"; import { useAuth } from "../../contexts/authContext";
import { useRouter } from "next/router";
import { AudioClip } from "@nirvana/common/models/conversation";
// New instance // New instance
const recorder = new MicRecorder({ const recorder = new MicRecorder({
@@ -22,72 +26,48 @@ const MAX_SHORTCUT_MAPPINGS_PRIORITY = 8;
export default function AudioHandler() { export default function AudioHandler() {
const { currUser } = useAuth(); const { currUser } = useAuth();
const router = useRouter();
const { convoId } = router.query;
const priorityConvos = useRecoilValue(priorityConvosSelector); const priorityConvos = useRecoilValue(priorityConvosSelector);
const mapAllConvos = useRecoilValue(allRelevantConversationsAtom);
const [mapShortcutsToConvoId, setmapShortcutsToConvoId] = useState< const [mapShortcutsToConvoId, setmapShortcutsToConvoId] = useState<
Map<string, string> Map<string, string>
>(new Map<string, string>()); >(new Map<string, string>());
const [selectedConvoId, setSelectedConversationId] = useRecoilState( const [selectedConvoId, setSelectedConversationId] =
selectedPriorityConvoAtom 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( const [hasMicPermissions, sethasMicPermissions] = useRecoilState(
hasMicPermissionsAtom 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(() => { useEffect(() => {
try { try {
// checking for permissions for recording
// won't work in https!!! // won't work in https!!!
navigator.mediaDevices navigator.mediaDevices
.getUserMedia({ audio: true }) .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<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]);
configure({ configure({
ignoreTags: ["input", "select", "textarea"], ignoreTags: ["input", "select", "textarea"],
}); });
@@ -205,9 +201,7 @@ export default function AudioHandler() {
return; return;
} }
const currConvo = priorityConvos.find( const currConvo = mapAllConvos.get(selectedConvoId);
(convo) => convo.id == selectedConvoId
);
if (!currConvo?.cachedAudioClip) { if (!currConvo?.cachedAudioClip) {
toast.error("nothing to play"); toast.error("nothing to play");
@@ -218,6 +212,29 @@ export default function AudioHandler() {
audio.play(); 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 = { const keyMap: KeyMap = {
SELECT_PRIORITY_CONVO: ["1", "2", "3", "4", "5", "6", "7", "8"], SELECT_PRIORITY_CONVO: ["1", "2", "3", "4", "5", "6", "7", "8"],
START_RECORDING: { START_RECORDING: {
@@ -2,7 +2,7 @@ import { Avatar, Tooltip } from "antd";
import { useRecoilState, useRecoilValue } from "recoil"; import { useRecoilState, useRecoilValue } from "recoil";
import { import {
checkIncomingMessageSelector, checkIncomingMessageSelector,
selectedPriorityConvoAtom, selectedConvoAtom,
} from "../../recoil/main"; } from "../../recoil/main";
import Conversation from "../../../common/models/conversation"; import Conversation from "../../../common/models/conversation";
import { MasterAvatarGroupWithUserFetch } from "../UserDetails/MasterAvatarGroup"; import { MasterAvatarGroupWithUserFetch } from "../UserDetails/MasterAvatarGroup";
@@ -23,9 +23,7 @@ export default function PriorityConvoRow(props: {
itemIndex: number; itemIndex: number;
}) { }) {
// figure out if this is a selectedConvo // figure out if this is a selectedConvo
const [selectedConvo, setSelectedConvo] = useRecoilState( const [selectedConvo, setSelectedConvo] = useRecoilState(selectedConvoAtom);
selectedPriorityConvoAtom
);
const isSelected = selectedConvo == props.conversation.id ? true : false; const isSelected = selectedConvo == props.conversation.id ? true : false;
@@ -2,10 +2,13 @@ import { yesterday } from "@nirvana/common/helpers/dateTime";
import { AudioClip } from "@nirvana/common/models/conversation"; import { AudioClip } from "@nirvana/common/models/conversation";
import moment from "moment"; import moment from "moment";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useRecoilValue } from "recoil"; import { useRecoilState, useRecoilValue } from "recoil";
import { import {
allRelevantContactsAtom, allRelevantContactsAtom,
allUsersConversationsAtom, allUsersConversationsAtom,
audioQueueAtom,
audioQueueCurrentClipSelector,
selectedConvoAtom,
} from "../../recoil/main"; } from "../../recoil/main";
import { MasterAvatarGroupWithUserFetch } from "../UserDetails/MasterAvatarGroup"; import { MasterAvatarGroupWithUserFetch } from "../UserDetails/MasterAvatarGroup";
import { useState } from "react"; import { useState } from "react";
@@ -21,6 +24,9 @@ export default function TimelineAudioClip(props: {
const audioClipUser = relContactsMap.get(props.audioClip.senderUserId); const audioClipUser = relContactsMap.get(props.audioClip.senderUserId);
const userConvoAssoc = userConvosMap.get(props.convoId); const userConvoAssoc = userConvosMap.get(props.convoId);
const audioQueueCurrentClip = useRecoilValue(audioQueueCurrentClipSelector);
const [audioQueue, setAudioQueue] = useRecoilState(audioQueueAtom);
const [audioDuration, setAudioDuration] = useState<number>(); const [audioDuration, setAudioDuration] = useState<number>();
const loadedMetadata = (data) => { const loadedMetadata = (data) => {
@@ -33,11 +39,21 @@ export default function TimelineAudioClip(props: {
// audio.onloadedmetadata = loadedMetadata; // audio.onloadedmetadata = loadedMetadata;
const playAudioClip = () => { const playAudioClip = () => {
toast(`${audioClipUser?.firstName} speaking...`); setAudioQueue((prevQueue) => {
return [...prevQueue, props.audioClip];
audio.play(); });
}; };
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 ( return (
<span <span
onClick={playAudioClip} onClick={playAudioClip}
@@ -47,12 +63,8 @@ export default function TimelineAudioClip(props: {
${ ${
props.index % 2 == 1 && props.index % 2 == 1 &&
"translate-y-[4.75rem] border-t-4 border-t-teal-600" "translate-y-[4.75rem] border-t-4 border-t-teal-600"
} ${props.index % 2 == 0 && "border-b-4 border-b-teal-600"} ${ } ${props.index % 2 == 0 && "border-b-4 border-b-teal-600"}
props.audioClip.createdDate < ${customClasses}`}
(userConvoAssoc?.lastInteractionDate || yesterday)
? "bg-slate-100 border"
: "bg-sky-100 "
}`}
style={{ style={{
minWidth: "max-content", minWidth: "max-content",
width: `100px`, width: `100px`,
@@ -1,7 +1,7 @@
import { QueryRoutes } from "@nirvana/common/helpers/routes"; import { QueryRoutes } from "@nirvana/common/helpers/routes";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { GlobalHotKeys, KeyMap, configure } from "react-hotkeys"; import { GlobalHotKeys, KeyMap, configure } from "react-hotkeys";
import { selectedPriorityConvoAtom } from "../../recoil/main"; import { selectedConvoAtom } from "../../recoil/main";
import { useSetRecoilState } from "recoil"; import { useSetRecoilState } from "recoil";
export default function KeyboardShortcutHandler() { export default function KeyboardShortcutHandler() {
@@ -11,7 +11,7 @@ export default function KeyboardShortcutHandler() {
ignoreTags: [], ignoreTags: [],
}); });
const setSelectedPriorityConvo = useSetRecoilState(selectedPriorityConvoAtom); const setSelectedPriorityConvo = useSetRecoilState(selectedConvoAtom);
const handleEscape = () => { const handleEscape = () => {
console.log("woah"); console.log("woah");
@@ -18,9 +18,10 @@ import {
} from "react-icons/fa"; } from "react-icons/fa";
import LinkIcon from "../Drawer/LinkIcon"; import LinkIcon from "../Drawer/LinkIcon";
import UserAvatar, { UserAvatarSizes } from "../UserDetails/UserAvatar"; import UserAvatar, { UserAvatarSizes } from "../UserDetails/UserAvatar";
import { useRecoilValue } from "recoil"; import { useRecoilValue, useSetRecoilState } from "recoil";
import { import {
allRelevantConversationsAtom, allRelevantConversationsAtom,
selectedConvoAtom,
userConvoAssociationSelector, userConvoAssociationSelector,
} from "../../recoil/main"; } from "../../recoil/main";
import { useRouter } from "next/router"; 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; const AUDIO_CLIP_FETCH_LIMIT = 50;
export default function ViewConvo(props: { conversationId: string }) { export default function ViewConvo(props: { conversationId: string }) {
@@ -158,7 +103,10 @@ export default function ViewConvo(props: { conversationId: string }) {
const [audioClips, setAudioClips] = useState<AudioClip[]>([] as AudioClip[]); const [audioClips, setAudioClips] = useState<AudioClip[]>([] as AudioClip[]);
const setSelectedConvoId = useSetRecoilState(selectedConvoAtom);
useEffect(() => { useEffect(() => {
// should have this convo, otherwise unauthorized
if (!convo || !convo.activeMembers.includes(currUser!.uid)) { if (!convo || !convo.activeMembers.includes(currUser!.uid)) {
toast.error("Not authorized for this conversation"); toast.error("Not authorized for this conversation");
@@ -170,6 +118,9 @@ export default function ViewConvo(props: { conversationId: string }) {
return; 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 // 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 // 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 // any new message is seamlessly added to the timeline
@@ -235,22 +186,21 @@ export default function ViewConvo(props: { conversationId: string }) {
{convo?.name} {convo?.name}
</span> </span>
<ConversationMemberStateIcon
convoUserAssocState={userConvoAssoc?.state}
/>
<span className="p-2 rounded-full hover:cursor-pointer hover:bg-slate-200 group-hover:visible invisible"> <span className="p-2 rounded-full hover:cursor-pointer hover:bg-slate-200 group-hover:visible invisible">
<FaEdit className="ml-auto text-md text-slate-400" /> <FaEdit className="ml-auto text-md text-slate-400" />
</span> </span>
</span> </span>
<span className="flex flex-row items-center space-x-2 hover:cursor-pointer group"> <span className="flex flex-row items-center space-x-2 hover:cursor-pointer group">
<MasterAvatarGroupWithUserFetch {convo?.activeMembers && (
listOfUserIds={convo?.activeMembers} <MasterAvatarGroupWithUserFetch
showCurrUser={true} listOfUserIds={convo.activeMembers}
maxUserCount={10} showCurrUser={true}
size={UserAvatarSizes.small} maxUserCount={10}
/> size={UserAvatarSizes.small}
/>
)}
<span className="text-xs text-slate-300 hover:decoration-slate-400 group-hover:underline "> <span className="text-xs text-slate-300 hover:decoration-slate-400 group-hover:underline ">
{convo?.activeMembers.length} members {convo?.activeMembers.length} members
</span> </span>
@@ -258,6 +208,12 @@ export default function ViewConvo(props: { conversationId: string }) {
</span> </span>
<span className="flex flex-row items-center space-x-3"> <span className="flex flex-row items-center space-x-3">
{userConvoAssoc?.state && (
<ConversationMemberStateIcon
convoUserAssocState={userConvoAssoc.state}
/>
)}
{userConvoAssoc?.state != ConversationMemberState.default && ( {userConvoAssoc?.state != ConversationMemberState.default && (
<button <button
onClick={() => onClick={() =>
@@ -92,6 +92,8 @@ export default function MasterAvatarGroup(props: {
export function MasterAvatarGroupWithUserFetch(props: { export function MasterAvatarGroupWithUserFetch(props: {
listOfUserIds: string[]; listOfUserIds: string[];
showCurrUser?: boolean; showCurrUser?: boolean;
maxUserCount?: number;
size?: UserAvatarSizes;
}) { }) {
const relContactsMap = useRecoilValue(allRelevantContactsAtom); const relContactsMap = useRecoilValue(allRelevantContactsAtom);
+22 -1
View File
@@ -48,6 +48,9 @@ export enum RecoilActions {
IS_RECORDING_ATOM = "IS_RECORDING_ATOM", IS_RECORDING_ATOM = "IS_RECORDING_ATOM",
HAS_MIC_PERMISSIONS = "HAS_MIC_PERMISSIONS", HAS_MIC_PERMISSIONS = "HAS_MIC_PERMISSIONS",
AUDIO_QUEUE_ATOM = "AUDIO_QUEUE_ATOM",
AUDIO_QUEUE_CURRENT_ITEM_SELECTOR = "AUDIO_QUEUE_CURRENT_ITEM_SELECTOR",
} }
export const nirvanaUserDataAtom = atom<NirvanaUser | null>({ export const nirvanaUserDataAtom = atom<NirvanaUser | null>({
@@ -410,7 +413,7 @@ export const isRecordingAtom = atom<boolean>({
default: false, default: false,
}); });
export const selectedPriorityConvoAtom = atom<string | null>({ export const selectedConvoAtom = atom<string | null>({
// convoId // convoId
key: RecoilActions.SELECTED_CONVERSATION_ATOM, key: RecoilActions.SELECTED_CONVERSATION_ATOM,
default: null, default: null,
@@ -420,3 +423,21 @@ export const hasMicPermissionsAtom = atom<boolean>({
key: RecoilActions.HAS_MIC_PERMISSIONS, key: RecoilActions.HAS_MIC_PERMISSIONS,
default: false, default: false,
}); });
export const audioQueueAtom = atom<AudioClip[]>({
key: RecoilActions.AUDIO_QUEUE_ATOM,
default: [] as AudioClip[],
});
export const audioQueueCurrentClipSelector = selector<AudioClip | null>({
key: RecoilActions.AUDIO_QUEUE_CURRENT_ITEM_SELECTOR,
get: ({ get }) => {
const currentQueue = get(audioQueueAtom);
if (currentQueue[0]) {
return currentQueue[0];
}
return null;
},
});