logic for uploading and all with magic and all
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Timestamp } from 'firebase/firestore';
|
import { Timestamp } from 'firebase/firestore';
|
||||||
|
|
||||||
interface IContent {
|
export interface IContent {
|
||||||
id: string;
|
id: string;
|
||||||
creatorUserId: string;
|
creatorUserId: string;
|
||||||
contentUrl: string; // remote resource of file/media
|
contentUrl: string; // remote resource of file/media
|
||||||
@@ -12,8 +12,9 @@ interface IContent {
|
|||||||
// ? persist length of clip for easier viewing for others...
|
// ? persist length of clip for easier viewing for others...
|
||||||
// ?they have to load it anyway and will get metadata anyway?
|
// ?they have to load it anyway and will get metadata anyway?
|
||||||
export class AudioClip implements IContent {
|
export class AudioClip implements IContent {
|
||||||
|
id: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public id: string,
|
|
||||||
public creatorUserId: string,
|
public creatorUserId: string,
|
||||||
public contentUrl: string,
|
public contentUrl: string,
|
||||||
public createdDate: Timestamp = Timestamp.now(),
|
public createdDate: Timestamp = Timestamp.now(),
|
||||||
@@ -21,8 +22,8 @@ export class AudioClip implements IContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Link implements IContent {
|
export class Link implements IContent {
|
||||||
|
id: string;
|
||||||
constructor(
|
constructor(
|
||||||
public id: string,
|
|
||||||
public creatorUserId: string,
|
public creatorUserId: string,
|
||||||
public contentUrl: string,
|
public contentUrl: string,
|
||||||
public createdDate: Timestamp = Timestamp.now(),
|
public createdDate: Timestamp = Timestamp.now(),
|
||||||
@@ -30,8 +31,9 @@ export class Link implements IContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Image implements IContent {
|
export class Image implements IContent {
|
||||||
|
id: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public id: string,
|
|
||||||
public creatorUserId: string,
|
public creatorUserId: string,
|
||||||
public contentUrl: string,
|
public contentUrl: string,
|
||||||
public thumbnailUrl?: string,
|
public thumbnailUrl?: string,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
FiLogOut,
|
FiLogOut,
|
||||||
FiMonitor,
|
FiMonitor,
|
||||||
FiWind,
|
FiWind,
|
||||||
|
FiCloudRain,
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ import {
|
|||||||
getConversationsQueryLIVE,
|
getConversationsQueryLIVE,
|
||||||
getUserById,
|
getUserById,
|
||||||
searchUsers,
|
searchUsers,
|
||||||
|
sendAudioClipToConversation,
|
||||||
} from '../firebase/firestore';
|
} from '../firebase/firestore';
|
||||||
import { Link, AudioClip, Image } from '@nirvana/core/src/models/content.model';
|
import { Link, AudioClip, Image } from '@nirvana/core/src/models/content.model';
|
||||||
import { useImmer } from 'use-immer';
|
import { useImmer } from 'use-immer';
|
||||||
@@ -66,6 +68,7 @@ import { useDebounce, useEffectOnce, useKeyPressEvent } from 'react-use';
|
|||||||
|
|
||||||
import KeyboardShortcutLabel from './KeyboardShortcutLabel';
|
import KeyboardShortcutLabel from './KeyboardShortcutLabel';
|
||||||
import Channels from '../electron/constants';
|
import Channels from '../electron/constants';
|
||||||
|
import { uploadAudioClip } from '../firebase/firebaseStorage';
|
||||||
type ConversationMap = {
|
type ConversationMap = {
|
||||||
[conversationId: string]: Conversation;
|
[conversationId: string]: Conversation;
|
||||||
};
|
};
|
||||||
@@ -89,12 +92,14 @@ interface ITerminalContext {
|
|||||||
getUser?: (userId: string) => Promise<User | undefined>;
|
getUser?: (userId: string) => Promise<User | undefined>;
|
||||||
|
|
||||||
isUserSpeaking: boolean;
|
isUserSpeaking: boolean;
|
||||||
|
isCloudDoingMagic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TerminalContext = React.createContext<ITerminalContext>({
|
const TerminalContext = React.createContext<ITerminalContext>({
|
||||||
conversationMap: {},
|
conversationMap: {},
|
||||||
|
|
||||||
isUserSpeaking: false,
|
isUserSpeaking: false,
|
||||||
|
isCloudDoingMagic: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// global audio data in memory
|
// global audio data in memory
|
||||||
@@ -103,13 +108,6 @@ const handleMediaRecorderData = (e: BlobEvent) => {
|
|||||||
audioChunks.push(e.data);
|
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) => {
|
const handleOnStartRecording = (e: BlobEvent) => {
|
||||||
audioChunks = [];
|
audioChunks = [];
|
||||||
};
|
};
|
||||||
@@ -267,6 +265,33 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
const [userAudioStream, setUserAudioStream] = useState<MediaStream>(null);
|
const [userAudioStream, setUserAudioStream] = useState<MediaStream>(null);
|
||||||
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder>(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(() => {
|
const handleAskForMicrophonePermissions = useCallback(() => {
|
||||||
// fix this...has to do with mac configuration
|
// fix this...has to do with mac configuration
|
||||||
// window.electronAPI.send(Channels.ASK_MICROPHONE_PERMISSIONS);
|
// 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' });
|
enqueueSnackbar('Check your audio mic permissions in preferences!!', { variant: 'error' });
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
}, [setUserAudioStream, enqueueSnackbar, setMediaRecorder]);
|
}, [setUserAudioStream, enqueueSnackbar, setMediaRecorder, handleOnStopRecording]);
|
||||||
|
|
||||||
// on load, try getting microphone access
|
// on load, try getting microphone access
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleAskForMicrophonePermissions();
|
handleAskForMicrophonePermissions();
|
||||||
}, [handleAskForMicrophonePermissions]);
|
}, [handleAskForMicrophonePermissions]);
|
||||||
|
|
||||||
// const handleRecorderStopped = useCallback(async () => {
|
|
||||||
// await
|
|
||||||
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
// when user wants to talk,
|
// when user wants to talk,
|
||||||
// unmute them and send clip for everyone to hear in the distance
|
// unmute them and send clip for everyone to hear in the distance
|
||||||
const handleBroadcast = useCallback(() => {
|
const handleBroadcast = useCallback(() => {
|
||||||
|
if (!selectedConversationId) {
|
||||||
|
enqueueSnackbar('You must select a conversation first!!!', { variant: 'warning' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!userAudioStream) {
|
if (!userAudioStream) {
|
||||||
enqueueSnackbar('Check your audio mic permissions in preferences!!', { variant: 'error' });
|
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?
|
// ? better to just start after 200 ms as put below for the delay of stopping recording?
|
||||||
if (mediaRecorder.state === 'recording') return;
|
if (mediaRecorder.state === 'recording') return;
|
||||||
|
|
||||||
mediaRecorder.start();
|
|
||||||
|
|
||||||
setIsUserSpeaking(true);
|
|
||||||
|
|
||||||
enqueueSnackbar('started recording');
|
enqueueSnackbar('started recording');
|
||||||
}, [setIsUserSpeaking, userAudioStream, mediaRecorder, handleAskForMicrophonePermissions]);
|
setIsCloudDoingMagic(true);
|
||||||
|
mediaRecorder.start();
|
||||||
|
setIsUserSpeaking(true);
|
||||||
|
}, [
|
||||||
|
selectedConversationId,
|
||||||
|
setIsUserSpeaking,
|
||||||
|
userAudioStream,
|
||||||
|
mediaRecorder,
|
||||||
|
handleAskForMicrophonePermissions,
|
||||||
|
setIsCloudDoingMagic,
|
||||||
|
enqueueSnackbar,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleStopBroadcast = useCallback(() => {
|
const handleStopBroadcast = useCallback(() => {
|
||||||
if (isUserSpeaking) {
|
if (isUserSpeaking) {
|
||||||
@@ -333,7 +364,7 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
mediaRecorder.stop();
|
mediaRecorder.stop();
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
}, [setIsUserSpeaking, isUserSpeaking]);
|
}, [setIsUserSpeaking, isUserSpeaking, mediaRecorder, enqueueSnackbar]);
|
||||||
|
|
||||||
useKeyPressEvent('`', handleBroadcast, handleStopBroadcast);
|
useKeyPressEvent('`', handleBroadcast, handleStopBroadcast);
|
||||||
|
|
||||||
@@ -386,6 +417,7 @@ export function TerminalProvider({ children }: { children?: React.ReactNode }) {
|
|||||||
getUser,
|
getUser,
|
||||||
handleQuickDial,
|
handleQuickDial,
|
||||||
selectConversation,
|
selectConversation,
|
||||||
|
isCloudDoingMagic,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Grid container spacing={0}>
|
<Grid container spacing={0}>
|
||||||
@@ -791,11 +823,12 @@ function MainPanel() {
|
|||||||
|
|
||||||
function ConversationDetails() {
|
function ConversationDetails() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { getUser, selectedConversation, selectConversation, isCloudDoingMagic } = useTerminal();
|
||||||
const { getUser, selectedConversation, selectConversation } = useTerminal();
|
|
||||||
|
|
||||||
const [conversationUsers, setConversationUsers] = useImmer<User[]>([]);
|
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
|
// join the room
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedConversation.membersInRoom?.length > 0) {
|
if (selectedConversation.membersInRoom?.length > 0) {
|
||||||
@@ -866,7 +899,7 @@ function ConversationDetails() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Container maxWidth={false} sx={{ position: 'relative', flex: 1 }}>
|
<Container maxWidth={false} sx={{ position: 'relative', flex: 1 }}>
|
||||||
{/* <ConversationHistory /> */}
|
<ConversationHistory />
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -887,6 +920,7 @@ function ConversationDetails() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ConversationHistory() {
|
function ConversationHistory() {
|
||||||
|
const { isCloudDoingMagic } = useTerminal();
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="xs">
|
<Container maxWidth="xs">
|
||||||
<Stack
|
<Stack
|
||||||
@@ -951,6 +985,12 @@ function ConversationHistory() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
{isCloudDoingMagic && (
|
||||||
|
<IconButton color="secondary">
|
||||||
|
<FiCloudRain />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
justifyContent={'flex-start'}
|
justifyContent={'flex-start'}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"apiKey": "AIzaSyAbTV9bb0TOtagzQMRMdXpSX5RhkS3z8NE",
|
"apiKey": "AIzaSyAbTV9bb0TOtagzQMRMdXpSX5RhkS3z8NE",
|
||||||
"authDomain": "nirvana-v3.firebaseapp.com",
|
"authDomain": "nirvana-v3.firebaseapp.com",
|
||||||
"projectId": "nirvana-v3",
|
"projectId": "nirvana-v3",
|
||||||
"storageBucket": "nirvana-v3.appspot.com",
|
"storageBucket": "gs://nirvana-v3.appspot.com/",
|
||||||
"messagingSenderId": "423533244953",
|
"messagingSenderId": "423533244953",
|
||||||
"appId": "1:423533244953:web:b492f91669886c065db088",
|
"appId": "1:423533244953:web:b492f91669886c065db088",
|
||||||
"measurementId": "G-0276T4D8NY",
|
"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,
|
MemberState,
|
||||||
} from '@nirvana/core/src/models/conversation.model';
|
} from '@nirvana/core/src/models/conversation.model';
|
||||||
import useAuth from '../providers/AuthProvider';
|
import useAuth from '../providers/AuthProvider';
|
||||||
|
import { AudioClip } from '@nirvana/core/src/models/content.model';
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -60,6 +61,9 @@ const db = {
|
|||||||
user: (userId: string) => docPoint<User>(`users/${userId}`),
|
user: (userId: string) => docPoint<User>(`users/${userId}`),
|
||||||
conversations: collectionPoint<Conversation>(`conversations`),
|
conversations: collectionPoint<Conversation>(`conversations`),
|
||||||
conversation: (conversationId: string) => docPoint<User>(`conversations/${conversationId}`),
|
conversation: (conversationId: string) => docPoint<User>(`conversations/${conversationId}`),
|
||||||
|
|
||||||
|
conversationAudioClips: (conversationId: string) =>
|
||||||
|
collectionPoint<AudioClip>(`conversations/${conversationId}/audioClips`),
|
||||||
};
|
};
|
||||||
|
|
||||||
// enum COLLECTION {
|
// enum COLLECTION {
|
||||||
@@ -194,3 +198,13 @@ export const createGroupConversation = async (
|
|||||||
throw new Error('Error creating user');
|
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