logic for uploading and all with magic and all
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<User | undefined>;
|
||||
|
||||
isUserSpeaking: boolean;
|
||||
isCloudDoingMagic: boolean;
|
||||
}
|
||||
|
||||
const TerminalContext = React.createContext<ITerminalContext>({
|
||||
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<MediaStream>(null);
|
||||
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder>(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<boolean>(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,
|
||||
}}
|
||||
>
|
||||
<Grid container spacing={0}>
|
||||
@@ -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<User[]>([]);
|
||||
|
||||
// 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() {
|
||||
</Stack>
|
||||
|
||||
<Container maxWidth={false} sx={{ position: 'relative', flex: 1 }}>
|
||||
{/* <ConversationHistory /> */}
|
||||
<ConversationHistory />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
@@ -887,6 +920,7 @@ function ConversationDetails() {
|
||||
}
|
||||
|
||||
function ConversationHistory() {
|
||||
const { isCloudDoingMagic } = useTerminal();
|
||||
return (
|
||||
<Container maxWidth="xs">
|
||||
<Stack
|
||||
@@ -951,6 +985,12 @@ function ConversationHistory() {
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
{isCloudDoingMagic && (
|
||||
<IconButton color="secondary">
|
||||
<FiCloudRain />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<Stack
|
||||
justifyContent={'flex-start'}
|
||||
alignItems={'center'}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"apiKey": "AIzaSyAbTV9bb0TOtagzQMRMdXpSX5RhkS3z8NE",
|
||||
"authDomain": "nirvana-v3.firebaseapp.com",
|
||||
"projectId": "nirvana-v3",
|
||||
"storageBucket": "nirvana-v3.appspot.com",
|
||||
"storageBucket": "gs://nirvana-v3.appspot.com/",
|
||||
"messagingSenderId": "423533244953",
|
||||
"appId": "1:423533244953:web:b492f91669886c065db088",
|
||||
"measurementId": "G-0276T4D8NY",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getDownloadURL, getStorage, ref, uploadBytes } from 'firebase/storage';
|
||||
|
||||
const storage = getStorage();
|
||||
|
||||
export const uploadAudioClip = async (uniqueFileName: string, blob: Blob) => {
|
||||
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');
|
||||
}
|
||||
};
|
||||
@@ -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<User>(`users/${userId}`),
|
||||
conversations: collectionPoint<Conversation>(`conversations`),
|
||||
conversation: (conversationId: string) => docPoint<User>(`conversations/${conversationId}`),
|
||||
|
||||
conversationAudioClips: (conversationId: string) =>
|
||||
collectionPoint<AudioClip>(`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');
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user