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 { 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<string, string>
>(new Map<string, string>());
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<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 {
// 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<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({
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: {
@@ -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;
@@ -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<number>();
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 (
<span
onClick={playAudioClip}
@@ -47,12 +63,8 @@ export default function TimelineAudioClip(props: {
${
props.index % 2 == 1 &&
"translate-y-[4.75rem] border-t-4 border-t-teal-600"
} ${props.index % 2 == 0 && "border-b-4 border-b-teal-600"} ${
props.audioClip.createdDate <
(userConvoAssoc?.lastInteractionDate || yesterday)
? "bg-slate-100 border"
: "bg-sky-100 "
}`}
} ${props.index % 2 == 0 && "border-b-4 border-b-teal-600"}
${customClasses}`}
style={{
minWidth: "max-content",
width: `100px`,
@@ -1,7 +1,7 @@
import { QueryRoutes } from "@nirvana/common/helpers/routes";
import { useRouter } from "next/router";
import { GlobalHotKeys, KeyMap, configure } from "react-hotkeys";
import { selectedPriorityConvoAtom } from "../../recoil/main";
import { selectedConvoAtom } from "../../recoil/main";
import { useSetRecoilState } from "recoil";
export default function KeyboardShortcutHandler() {
@@ -11,7 +11,7 @@ export default function KeyboardShortcutHandler() {
ignoreTags: [],
});
const setSelectedPriorityConvo = useSetRecoilState(selectedPriorityConvoAtom);
const setSelectedPriorityConvo = useSetRecoilState(selectedConvoAtom);
const handleEscape = () => {
console.log("woah");
@@ -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<AudioClip[]>([] 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}
</span>
<ConversationMemberStateIcon
convoUserAssocState={userConvoAssoc?.state}
/>
<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" />
</span>
</span>
<span className="flex flex-row items-center space-x-2 hover:cursor-pointer group">
<MasterAvatarGroupWithUserFetch
listOfUserIds={convo?.activeMembers}
showCurrUser={true}
maxUserCount={10}
size={UserAvatarSizes.small}
/>
{convo?.activeMembers && (
<MasterAvatarGroupWithUserFetch
listOfUserIds={convo.activeMembers}
showCurrUser={true}
maxUserCount={10}
size={UserAvatarSizes.small}
/>
)}
<span className="text-xs text-slate-300 hover:decoration-slate-400 group-hover:underline ">
{convo?.activeMembers.length} members
</span>
@@ -258,6 +208,12 @@ export default function ViewConvo(props: { conversationId: string }) {
</span>
<span className="flex flex-row items-center space-x-3">
{userConvoAssoc?.state && (
<ConversationMemberStateIcon
convoUserAssocState={userConvoAssoc.state}
/>
)}
{userConvoAssoc?.state != ConversationMemberState.default && (
<button
onClick={() =>
@@ -92,6 +92,8 @@ export default function MasterAvatarGroup(props: {
export function MasterAvatarGroupWithUserFetch(props: {
listOfUserIds: string[];
showCurrUser?: boolean;
maxUserCount?: number;
size?: UserAvatarSizes;
}) {
const relContactsMap = useRecoilValue(allRelevantContactsAtom);
+22 -1
View File
@@ -48,6 +48,9 @@ export enum RecoilActions {
IS_RECORDING_ATOM = "IS_RECORDING_ATOM",
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>({
@@ -410,7 +413,7 @@ export const isRecordingAtom = atom<boolean>({
default: false,
});
export const selectedPriorityConvoAtom = atom<string | null>({
export const selectedConvoAtom = atom<string | null>({
// convoId
key: RecoilActions.SELECTED_CONVERSATION_ATOM,
default: null,
@@ -420,3 +423,21 @@ export const hasMicPermissionsAtom = atom<boolean>({
key: RecoilActions.HAS_MIC_PERMISSIONS,
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;
},
});