diff --git a/packages/core/src/models/content.model.ts b/packages/core/src/models/content.model.ts index 9cb156e..0ed6c04 100644 --- a/packages/core/src/models/content.model.ts +++ b/packages/core/src/models/content.model.ts @@ -1,6 +1,6 @@ import { Timestamp } from 'firebase/firestore'; -interface IContent { +export interface IContent { id: string; creatorUserId: string; contentUrl: string; // remote resource of file/media @@ -12,8 +12,9 @@ interface IContent { // ? persist length of clip for easier viewing for others... // ?they have to load it anyway and will get metadata anyway? export class AudioClip implements IContent { + id: string; + constructor( - public id: string, public creatorUserId: string, public contentUrl: string, public createdDate: Timestamp = Timestamp.now(), @@ -21,8 +22,8 @@ export class AudioClip implements IContent { } export class Link implements IContent { + id: string; constructor( - public id: string, public creatorUserId: string, public contentUrl: string, public createdDate: Timestamp = Timestamp.now(), @@ -30,8 +31,9 @@ export class Link implements IContent { } export class Image implements IContent { + id: string; + constructor( - public id: string, public creatorUserId: string, public contentUrl: string, public thumbnailUrl?: string, diff --git a/packages/desktop/src/components/Terminal.tsx b/packages/desktop/src/components/Terminal.tsx index 89b6ffb..2f1b35d 100644 --- a/packages/desktop/src/components/Terminal.tsx +++ b/packages/desktop/src/components/Terminal.tsx @@ -43,6 +43,7 @@ import { FiLogOut, FiMonitor, FiWind, + FiCloudRain, } from 'react-icons/fi'; import { useSnackbar } from 'notistack'; @@ -55,6 +56,7 @@ import { getConversationsQueryLIVE, getUserById, searchUsers, + sendAudioClipToConversation, } from '../firebase/firestore'; import { Link, AudioClip, Image } from '@nirvana/core/src/models/content.model'; import { useImmer } from 'use-immer'; @@ -66,6 +68,7 @@ import { useDebounce, useEffectOnce, useKeyPressEvent } from 'react-use'; import KeyboardShortcutLabel from './KeyboardShortcutLabel'; import Channels from '../electron/constants'; +import { uploadAudioClip } from '../firebase/firebaseStorage'; type ConversationMap = { [conversationId: string]: Conversation; }; @@ -89,12 +92,14 @@ interface ITerminalContext { getUser?: (userId: string) => Promise; isUserSpeaking: boolean; + isCloudDoingMagic: boolean; } const TerminalContext = React.createContext({ conversationMap: {}, isUserSpeaking: false, + isCloudDoingMagic: false, }); // global audio data in memory @@ -103,13 +108,6 @@ const handleMediaRecorderData = (e: BlobEvent) => { audioChunks.push(e.data); }; -const handleOnStopRecording = (e: BlobEvent) => { - console.log(audioChunks); - const blob = new Blob(audioChunks); - const audioUrl = URL.createObjectURL(blob); - const player = new Audio(audioUrl); - player.autoplay = true; -}; const handleOnStartRecording = (e: BlobEvent) => { audioChunks = []; }; @@ -267,6 +265,33 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { const [userAudioStream, setUserAudioStream] = useState(null); const [mediaRecorder, setMediaRecorder] = useState(null); + // from the time of starting to speak to finally sent to everyone out there + // feeling to user that everyone is listening right now even though it's only recording + const [isCloudDoingMagic, setIsCloudDoingMagic] = useState(false); + + const handleOnStopRecording = useCallback(async () => { + console.log(audioChunks[0]); + const blob = new Blob(audioChunks); + + const audioUrl = URL.createObjectURL(blob); + const player = new Audio(audioUrl); + player.autoplay = true; + + try { + const downloadUrl = await uploadAudioClip(`${user.uid}-${new Date().valueOf()}`, blob); + + const audioClip = new AudioClip(user.uid, downloadUrl); + await sendAudioClipToConversation(audioClip, selectedConversation.id); + + enqueueSnackbar('clip sent!', { variant: 'success' }); + setIsCloudDoingMagic(false); + } catch (error) { + console.error(error); + enqueueSnackbar('Problem in recording clip', { variant: 'error' }); + } + enqueueSnackbar('clip sent!', { variant: 'success' }); + }, [user.uid, setIsCloudDoingMagic, enqueueSnackbar, selectedConversation?.id]); + const handleAskForMicrophonePermissions = useCallback(() => { // fix this...has to do with mac configuration // window.electronAPI.send(Channels.ASK_MICROPHONE_PERMISSIONS); @@ -290,21 +315,20 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { enqueueSnackbar('Check your audio mic permissions in preferences!!', { variant: 'error' }); console.error(error); }); - }, [setUserAudioStream, enqueueSnackbar, setMediaRecorder]); + }, [setUserAudioStream, enqueueSnackbar, setMediaRecorder, handleOnStopRecording]); // on load, try getting microphone access useEffect(() => { handleAskForMicrophonePermissions(); }, [handleAskForMicrophonePermissions]); - // const handleRecorderStopped = useCallback(async () => { - // await - - // }, []) - // when user wants to talk, // unmute them and send clip for everyone to hear in the distance const handleBroadcast = useCallback(() => { + if (!selectedConversationId) { + enqueueSnackbar('You must select a conversation first!!!', { variant: 'warning' }); + } + if (!userAudioStream) { enqueueSnackbar('Check your audio mic permissions in preferences!!', { variant: 'error' }); @@ -316,12 +340,19 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { // ? better to just start after 200 ms as put below for the delay of stopping recording? if (mediaRecorder.state === 'recording') return; - mediaRecorder.start(); - - setIsUserSpeaking(true); - enqueueSnackbar('started recording'); - }, [setIsUserSpeaking, userAudioStream, mediaRecorder, handleAskForMicrophonePermissions]); + setIsCloudDoingMagic(true); + mediaRecorder.start(); + setIsUserSpeaking(true); + }, [ + selectedConversationId, + setIsUserSpeaking, + userAudioStream, + mediaRecorder, + handleAskForMicrophonePermissions, + setIsCloudDoingMagic, + enqueueSnackbar, + ]); const handleStopBroadcast = useCallback(() => { if (isUserSpeaking) { @@ -333,7 +364,7 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { mediaRecorder.stop(); }, 200); } - }, [setIsUserSpeaking, isUserSpeaking]); + }, [setIsUserSpeaking, isUserSpeaking, mediaRecorder, enqueueSnackbar]); useKeyPressEvent('`', handleBroadcast, handleStopBroadcast); @@ -386,6 +417,7 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) { getUser, handleQuickDial, selectConversation, + isCloudDoingMagic, }} > @@ -791,11 +823,12 @@ function MainPanel() { function ConversationDetails() { const { user } = useAuth(); - - const { getUser, selectedConversation, selectConversation } = useTerminal(); - + const { getUser, selectedConversation, selectConversation, isCloudDoingMagic } = useTerminal(); const [conversationUsers, setConversationUsers] = useImmer([]); + // TODO: at the terminal level, make sure we are listening for audio clips + // and here it's just an access of that map of content for each conversation's blocks + // join the room useEffect(() => { if (selectedConversation.membersInRoom?.length > 0) { @@ -866,7 +899,7 @@ function ConversationDetails() { - {/* */} + + {isCloudDoingMagic && ( + + + + )} + { + try { + const audioClipsRef = ref(storage, `audioClips/${uniqueFileName}`); + + // 'file' comes from the Blob or File API + const snapshot = await uploadBytes(audioClipsRef, blob); + + return await getDownloadURL(audioClipsRef); + } catch (error) { + console.warn('problem in uploading audio clip'); + + throw Error('problem in uploading audio clip'); + } +}; diff --git a/packages/desktop/src/firebase/firestore.ts b/packages/desktop/src/firebase/firestore.ts index e13a4a5..a418402 100644 --- a/packages/desktop/src/firebase/firestore.ts +++ b/packages/desktop/src/firebase/firestore.ts @@ -29,6 +29,7 @@ import Conversation, { MemberState, } from '@nirvana/core/src/models/conversation.model'; import useAuth from '../providers/AuthProvider'; +import { AudioClip } from '@nirvana/core/src/models/content.model'; interface Document { id: string; @@ -60,6 +61,9 @@ const db = { user: (userId: string) => docPoint(`users/${userId}`), conversations: collectionPoint(`conversations`), conversation: (conversationId: string) => docPoint(`conversations/${conversationId}`), + + conversationAudioClips: (conversationId: string) => + collectionPoint(`conversations/${conversationId}/audioClips`), }; // enum COLLECTION { @@ -194,3 +198,13 @@ export const createGroupConversation = async ( throw new Error('Error creating user'); } }; + +export const sendAudioClipToConversation = async (audioClip: AudioClip, conversationId: string) => { + try { + await addDoc(db.conversationAudioClips(conversationId), audioClip); + } catch (e) { + console.error('Error creating user: ', e); + + throw new Error('Error creating user'); + } +};