763 lines
26 KiB
TypeScript
763 lines
26 KiB
TypeScript
import {
|
|
ConnectToLineRequest,
|
|
RtcAnswerSomeoneRequest,
|
|
RtcCallRequest,
|
|
RtcNewUserJoinedResponse,
|
|
RtcReceiveAnswerResponse,
|
|
ServerRequestChannels,
|
|
ServerResponseChannels,
|
|
SomeoneConnectedResponse,
|
|
SomeoneDisconnectedResponse,
|
|
SomeoneTunedResponse,
|
|
SomeoneUntunedFromLineResponse,
|
|
TuneToLineRequest,
|
|
UntuneFromLineRequest,
|
|
} from '@nirvana/core/sockets/channels';
|
|
import Conversation, { MemberState } from '@nirvana/core/models/conversation.model';
|
|
import { ConversationMap, MasterConversation } from '../util/types';
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { Updater, useImmer } from 'use-immer';
|
|
import {
|
|
checkIfOnOnOneExists,
|
|
createConversation,
|
|
getConversationById,
|
|
getConversations,
|
|
} from '../api/NirvanaApi';
|
|
import { useAsyncFn, useEffectOnce, useUnmount } from 'react-use';
|
|
|
|
import CreateConversationRequest from '@nirvana/core/requests/CreateConversationRequest.request';
|
|
import Peer from 'simple-peer';
|
|
import { Typography } from '@mui/material';
|
|
import User from '@nirvana/core/models/user.model';
|
|
import toast from 'react-hot-toast';
|
|
import useAuth from './AuthProvider';
|
|
import useSockets from './SocketProvider';
|
|
|
|
// responsible for firing socket events from the local client
|
|
function useSocketFire() {
|
|
const { $ws } = useSockets();
|
|
|
|
const handleConnectToLine = useCallback(
|
|
(lineId: string) => {
|
|
$ws.emit(ServerRequestChannels.CONNECT_TO_LINE, new ConnectToLineRequest(lineId));
|
|
},
|
|
[$ws],
|
|
);
|
|
|
|
const handleTuneIntoLine = useCallback(
|
|
(lineId: string) => {
|
|
$ws.emit(ServerRequestChannels.TUNE_INTO_LINE, new TuneToLineRequest(lineId));
|
|
},
|
|
[$ws],
|
|
);
|
|
|
|
const handleUntuneFromLine = useCallback(
|
|
(lineId: string) => {
|
|
$ws.emit(ServerRequestChannels.UNTUNE_FROM_LINE, new UntuneFromLineRequest(lineId));
|
|
},
|
|
[$ws],
|
|
);
|
|
|
|
return { handleConnectToLine, handleTuneIntoLine, handleUntuneFromLine };
|
|
}
|
|
|
|
interface IConversationContext {
|
|
conversationMap: ConversationMap;
|
|
|
|
selectedConversation?: MasterConversation;
|
|
selectConversation?: (conversationId: string, temporaryOverrideSort?: boolean) => Promise<void>;
|
|
|
|
handleStartConversation?: (otherUsers: User[], conversationName?: string) => Promise<boolean>;
|
|
}
|
|
|
|
const ConversationContext = React.createContext<IConversationContext>({
|
|
conversationMap: {},
|
|
});
|
|
|
|
export function ConversationProvider({ children }: { children: React.ReactNode }) {
|
|
const { user } = useAuth();
|
|
|
|
// the initial persistent fetch for conversations
|
|
const [fetchState, doFetch] = useAsyncFn(getConversations);
|
|
|
|
// the map that we manage throughout the app
|
|
const [conversationMap, setConversationMap] = useImmer<ConversationMap>({});
|
|
|
|
const [selectedConversation, setSelectedConversation] = useState<MasterConversation>(undefined);
|
|
|
|
const { $ws } = useSockets();
|
|
const { handleConnectToLine, handleTuneIntoLine, handleUntuneFromLine } = useSocketFire();
|
|
|
|
// fetch conversations
|
|
useEffect(() => {
|
|
doFetch();
|
|
}, [doFetch]);
|
|
|
|
// conversation socket listeners
|
|
useEffect(() => {
|
|
$ws.on(ServerResponseChannels.SOMEONE_CONNECTED_TO_LINE, (res: SomeoneConnectedResponse) => {
|
|
setConversationMap((draft) => {
|
|
if (!draft[res.lineId]) {
|
|
toast.error('no line');
|
|
return;
|
|
}
|
|
|
|
draft[res.lineId].connectedUserIds = res.allConnectedUsers;
|
|
draft[res.lineId].tunedInUsers = res.allTunedUsers;
|
|
});
|
|
});
|
|
|
|
$ws.on(ServerResponseChannels.SOMEONE_TUNED_INTO_LINE, (res: SomeoneTunedResponse) => {
|
|
setConversationMap((draft) => {
|
|
if (!draft[res.lineId]) {
|
|
toast.error('no line');
|
|
return;
|
|
}
|
|
|
|
draft[res.lineId].tunedInUsers = res.allUsers;
|
|
});
|
|
});
|
|
|
|
$ws.on(
|
|
ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE,
|
|
(res: SomeoneUntunedFromLineResponse) => {
|
|
setConversationMap((draft) => {
|
|
if (!draft[res.lineId]) {
|
|
toast.error('no line');
|
|
return;
|
|
}
|
|
|
|
draft[res.lineId].tunedInUsers = draft[res.lineId].tunedInUsers.filter(
|
|
(userId) => userId !== res.userId,
|
|
);
|
|
});
|
|
},
|
|
);
|
|
}, [$ws, setConversationMap]);
|
|
|
|
// remove them from the line connected list and tuned list if they are there
|
|
$ws.on(
|
|
ServerResponseChannels.SOMEONE_DISCONNECTED_FROM_LINE,
|
|
(res: SomeoneDisconnectedResponse) => {
|
|
setConversationMap((draft) => {
|
|
if (!draft[res.lineId]) {
|
|
toast.error('there was a problem updating rooms!!!');
|
|
return;
|
|
}
|
|
|
|
draft[res.lineId].connectedUserIds = draft[res.lineId].connectedUserIds?.filter(
|
|
(userId) => userId !== res.userId,
|
|
);
|
|
draft[res.lineId].tunedInUsers = draft[res.lineId].tunedInUsers?.filter(
|
|
(userId) => userId !== res.userId,
|
|
);
|
|
});
|
|
},
|
|
);
|
|
|
|
// add the conversation to the conversation map/client side cache
|
|
// has implications on realtime listening and also the ui on whether or not it's shown
|
|
const handleAddConversationCache = useCallback(
|
|
(conversation: Conversation, temporaryOverrideSort = false) => {
|
|
setConversationMap((draft) => {
|
|
draft[conversation._id.toString()] = {
|
|
...conversation,
|
|
tunedInUsers: [],
|
|
connectedUserIds: [],
|
|
temporaryOverrideSort,
|
|
};
|
|
});
|
|
|
|
handleConnectToLine(conversation._id.toString());
|
|
|
|
// if it's in my priority list, tune into it automatically
|
|
const currMemberForConversation = conversation.members.find(
|
|
(mem) => mem.email === user.email,
|
|
);
|
|
if (currMemberForConversation && currMemberForConversation.memberState === 'priority') {
|
|
handleTuneIntoLine(conversation._id.toString());
|
|
}
|
|
},
|
|
[setConversationMap, handleConnectToLine, handleTuneIntoLine, user],
|
|
);
|
|
|
|
// add conversations to cache on initial fetch
|
|
// the two stores | persistent and socket | are decoupled
|
|
useEffect(() => {
|
|
if (fetchState.value?.data) {
|
|
fetchState.value.data.forEach((conversationResult) => {
|
|
handleAddConversationCache(conversationResult);
|
|
});
|
|
}
|
|
}, [fetchState.value, handleAddConversationCache]);
|
|
|
|
/**
|
|
* temporaryOverrideSort: tell me if you want to temporarily prioritize this conversation above all others
|
|
*/
|
|
// selecting an id and making sure that it's in the map and the views just use the id to access from map
|
|
// get conversation from universe...hit if it's local, miss and fetch if not here
|
|
const selectConversation = useCallback(
|
|
async (conversationId: string, temporaryOverrideSort = false) => {
|
|
/**
|
|
* see if there is such a conversation in our map
|
|
* if not, then go and fetch it from backend
|
|
* set the temporary sort accordingly
|
|
*/
|
|
|
|
const allConversationIds = Object.keys(conversationMap);
|
|
let conversationToSelect: MasterConversation;
|
|
|
|
if (allConversationIds.includes(conversationId)) {
|
|
toast.success('we have that conversation!!!');
|
|
|
|
conversationToSelect = conversationMap[conversationId];
|
|
} else {
|
|
// TODO: somehow use handleAddToCache instead of this duplicated code
|
|
|
|
// fetch and add to master conversation cache
|
|
const retrieveConversationResult = await getConversationById(conversationId);
|
|
if (!retrieveConversationResult.data) {
|
|
toast.error('unable to find conversation');
|
|
return;
|
|
}
|
|
|
|
const newMasterConversation: MasterConversation = {
|
|
...retrieveConversationResult.data,
|
|
tunedInUsers: [],
|
|
connectedUserIds: [],
|
|
temporaryOverrideSort,
|
|
};
|
|
|
|
setConversationMap((draft) => {
|
|
draft[conversationId] = newMasterConversation;
|
|
});
|
|
|
|
conversationToSelect = newMasterConversation;
|
|
|
|
// connect to line so that we can listen in to changes
|
|
handleConnectToLine(newMasterConversation._id.toString());
|
|
}
|
|
|
|
// set selected conversation with full master conversation now that we have it
|
|
setSelectedConversation((prevConversation) => {
|
|
// untune if it was an inbox conversation and we are going away from this conversation
|
|
if (prevConversation) {
|
|
const currMemberForConversation = prevConversation.members.find(
|
|
(mem) => mem.email === user.email,
|
|
);
|
|
|
|
if (currMemberForConversation && currMemberForConversation.memberState === 'inbox') {
|
|
handleUntuneFromLine(prevConversation._id.toString());
|
|
}
|
|
}
|
|
|
|
handleTuneIntoLine(conversationToSelect._id.toString());
|
|
|
|
return conversationToSelect;
|
|
});
|
|
},
|
|
[
|
|
user,
|
|
conversationMap,
|
|
setSelectedConversation,
|
|
setConversationMap,
|
|
handleUntuneFromLine,
|
|
handleTuneIntoLine,
|
|
handleConnectToLine,
|
|
],
|
|
);
|
|
// !temporary fix for updated conversation content not being surfaced
|
|
// as the map value updates
|
|
useEffect(() => {
|
|
if (selectedConversation) {
|
|
setSelectedConversation(conversationMap[selectedConversation._id.toString()]);
|
|
}
|
|
}, [conversationMap, selectedConversation, setSelectedConversation]);
|
|
|
|
// handling quick dial or creating a conversation
|
|
const handleStartConversation = useCallback(
|
|
async (otherUsers: User[], conversationName?: string) => {
|
|
try {
|
|
/** group conversation
|
|
* 1. create it
|
|
* 2. select the conversation based on the id
|
|
*
|
|
* one on one:
|
|
* 1. hit backend to see if we already have a conversation with them
|
|
* 2. if so, then go and select it with priority sort flag
|
|
* 3. if not, then go ahead and create
|
|
* 4. use the conversation id to select it
|
|
*/
|
|
|
|
if (!otherUsers || otherUsers.length === 0) {
|
|
toast.error('Must provider users');
|
|
return false;
|
|
}
|
|
|
|
// todo: check to make sure that the other user is not the user himself, although backend should error out
|
|
|
|
if (otherUsers.length === 1) {
|
|
console.log('checking if exists....');
|
|
|
|
const conversationDmCheck = await checkIfOnOnOneExists(otherUsers[0]._id.toString());
|
|
|
|
console.log(conversationDmCheck);
|
|
|
|
// if we found a conversation
|
|
if (conversationDmCheck.data) {
|
|
toast.success('already have this user...quick dialing');
|
|
|
|
selectConversation(conversationDmCheck.data, true);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const createdConversationResult = await createConversation(
|
|
new CreateConversationRequest(otherUsers, conversationName),
|
|
);
|
|
|
|
selectConversation(createdConversationResult.data.conversationId.toString(), true);
|
|
|
|
toast.success('started conversation');
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error(error);
|
|
toast.error(error.message);
|
|
}
|
|
return false;
|
|
},
|
|
[selectConversation],
|
|
);
|
|
|
|
const userTunedInConversations = useMemo(() => {
|
|
const tunedConversations: MasterConversation[] = [];
|
|
|
|
Object.values(conversationMap).forEach((currentMasterConversation) => {
|
|
if (currentMasterConversation.tunedInUsers.includes(user._id.toString())) {
|
|
tunedConversations.push(currentMasterConversation);
|
|
}
|
|
});
|
|
|
|
return tunedConversations;
|
|
}, [conversationMap, user]);
|
|
|
|
if (fetchState.error) {
|
|
return (
|
|
<Typography variant={'h6'} color={'danger'}>
|
|
Something went terribly wrong
|
|
</Typography>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ConversationContext.Provider
|
|
value={{
|
|
conversationMap,
|
|
handleStartConversation,
|
|
selectedConversation,
|
|
selectConversation,
|
|
}}
|
|
>
|
|
{children}
|
|
|
|
{/* with id specified, shouldn't unmount and mount unexpectedly */}
|
|
{userTunedInConversations.map((tunedConversation) => (
|
|
<Room
|
|
key={`streamRoom-${tunedConversation._id.toString()}`}
|
|
setConversationMap={setConversationMap}
|
|
conversation={tunedConversation}
|
|
/>
|
|
))}
|
|
</ConversationContext.Provider>
|
|
);
|
|
}
|
|
|
|
export default function useConversations() {
|
|
return React.useContext(ConversationContext);
|
|
}
|
|
|
|
const videoConstraints = {
|
|
frameRate: 15,
|
|
width: { max: 100 },
|
|
height: { max: 200 },
|
|
};
|
|
|
|
const iceServers = [
|
|
// { urls: 'stun:stun.l.google.com:19302' },
|
|
// { urls: 'stun:stun.l.google.com:19302' },
|
|
// { urls: 'stun:stun1.l.google.com:19302' },
|
|
{ urls: 'stun:stun2.l.google.com:19302' },
|
|
// { urls: 'stun:stun3.l.google.com:19302' },
|
|
// { urls: 'stun:stun4.l.google.com:19302' },
|
|
// { urls: 'stun:global.stun.twilio.com:3478?transport=udp' },
|
|
// {
|
|
// url: 'turn:numb.viagenie.ca',
|
|
// credential: 'muazkh',
|
|
// username: '[email protected]',
|
|
// },
|
|
// {
|
|
// url: 'turn:192.158.29.39:3478?transport=udp',
|
|
// credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
|
|
// username: '28224511:1379330808',
|
|
// },
|
|
{
|
|
url: 'turn:turn.bistri.com:80',
|
|
credential: 'homeo',
|
|
username: 'homeo',
|
|
},
|
|
|
|
// {
|
|
// url: 'turn:turn.anyfirewall.com:443?transport=tcp',
|
|
// credential: 'webrtc',
|
|
// username: 'webrtc',
|
|
// },
|
|
// {
|
|
// url: 'turn:openrelay.metered.ca:80',
|
|
// credential: 'openrelayproject',
|
|
// username: 'openrelayproject',
|
|
// },
|
|
];
|
|
|
|
// renderless component to manage a room and send data upward for other components
|
|
// - decoupled from sockets and other data, render if you want to join x room
|
|
// - unmount if you want to leave room
|
|
// - have event listeners specific to this room
|
|
// - take in new users that come into the room
|
|
function Room({
|
|
conversation,
|
|
setConversationMap,
|
|
}: {
|
|
conversation: MasterConversation;
|
|
mediaStream?: MediaStream;
|
|
setConversationMap: Updater<ConversationMap>;
|
|
}) {
|
|
const { user } = useAuth();
|
|
const { $ws } = useSockets();
|
|
|
|
const [userLocalStream, setUserLocalStream] = useState<MediaStream>();
|
|
|
|
// have internal state to manage details of this "room"
|
|
|
|
// ============== STREAMING ===============
|
|
|
|
// handle incoming calls and accept calls and create objects for them
|
|
useEffectOnce(() => {
|
|
(async () => {
|
|
// TODO : bring this stream object higher and add this later to the room
|
|
// set up local stream for this room - temporary until we figure out global stream handling and passing down
|
|
const localMediaStream = await navigator.mediaDevices.getUserMedia({
|
|
video: videoConstraints,
|
|
audio: true,
|
|
});
|
|
|
|
console.log(localMediaStream);
|
|
|
|
setUserLocalStream(localMediaStream);
|
|
|
|
// ?will there be race condition where this room component is rendered but we don't have the latest
|
|
// ?list of tunedin folks and so we may just end up calling select few?
|
|
// ?in this case, start with initiating event to get all people in room first
|
|
|
|
if (conversation.tunedInUsers && conversation.tunedInUsers.length > 1) {
|
|
// going ahead and calling all of the other folks
|
|
const allOtherUserIds = conversation.tunedInUsers.filter(
|
|
(memberUserId) => memberUserId !== user._id.toString(),
|
|
);
|
|
toast.success('calling bunch of people');
|
|
|
|
// for each person, create peer object
|
|
allOtherUserIds.forEach((otherUserId) => {
|
|
const connectingToast = toast.loading('calling peer for a snappy experience');
|
|
|
|
// ========= PEER CREATION =============
|
|
// make sure this peer gets destroyed when it's time to remove this listener
|
|
const localPeerConnection = new Peer({
|
|
initiator: true,
|
|
stream: localMediaStream,
|
|
trickle: false, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times,
|
|
config: {
|
|
iceServers,
|
|
},
|
|
});
|
|
|
|
// ========= PEER EVENT HANDLERS =============
|
|
localPeerConnection.on('signal', (signal) => {
|
|
console.log('have a signal to make call to someone ');
|
|
|
|
$ws.emit(
|
|
ServerRequestChannels.RTC_CALL_SOMEONE_FOR_LINE,
|
|
new RtcCallRequest(otherUserId, conversation._id.toString(), signal),
|
|
);
|
|
|
|
toast.dismiss(connectingToast);
|
|
|
|
// todo, take trickle into account to not overwrite stream
|
|
// notifying globally that we are in the process now
|
|
setConversationMap((draft) => {
|
|
if (draft[conversation._id.toString()]) {
|
|
draft[conversation._id.toString()].room = {
|
|
...(draft[conversation._id.toString()].room ?? {}),
|
|
[otherUserId]: { peer: localPeerConnection, isConnecting: true },
|
|
};
|
|
}
|
|
});
|
|
});
|
|
|
|
localPeerConnection.on('stream', (remoteStream: MediaStream) => {
|
|
// globally updating conversation so that other views can render what they want
|
|
toast.success('got stream from remote, going to add to our ');
|
|
|
|
console.log('got stream from master');
|
|
|
|
setConversationMap((draft) => {
|
|
if (draft[conversation._id.toString()].room[otherUserId]) {
|
|
draft[conversation._id.toString()].room[otherUserId].stream = remoteStream;
|
|
|
|
remoteStream.getTracks().forEach((track) => {
|
|
// TODO: add particular track to right place
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
localPeerConnection.on('connect', () => {
|
|
toast.success('successfully connected to another peer');
|
|
setConversationMap((draft) => {
|
|
if (draft[conversation._id.toString()].room[otherUserId]) {
|
|
draft[conversation._id.toString()].room[otherUserId].isConnecting = false;
|
|
}
|
|
});
|
|
});
|
|
|
|
localPeerConnection.on('track', (track, stream) => {
|
|
// TODO: add to room contents
|
|
|
|
toast('a peer added a track to a stream');
|
|
});
|
|
|
|
localPeerConnection.on('close', () => {
|
|
// the person will be removed from the tuned in list, but the connections here are decoupled from that flow
|
|
// we want to manage the room within the master conversation and remove it for ourselves
|
|
// !this is false, see unmount, we are relying on tuned list for disconnections
|
|
|
|
setConversationMap((draft) => {
|
|
if (draft[conversation._id.toString()].room[otherUserId]) {
|
|
draft[conversation._id.toString()].room[otherUserId].peer?.destroy();
|
|
delete draft[conversation._id.toString()].room[otherUserId];
|
|
}
|
|
});
|
|
|
|
toast.error('peer connection was closed');
|
|
});
|
|
|
|
localPeerConnection.on('error', (err) => {
|
|
console.error(err);
|
|
toast.error('there was a problem with the peer connection');
|
|
});
|
|
});
|
|
}
|
|
})();
|
|
});
|
|
|
|
// ======= LISTEN FOR INCOMING CALLS AND RETURN SIGNALS AND ACCEPT THEM
|
|
useEffect(() => {
|
|
const someoneJoinedChannelNameForRoom = `${
|
|
ServerResponseChannels.RTC_NEW_USER_JOINED
|
|
}:${conversation._id.toString()}`;
|
|
const mastersAnswerReceivedChannelNameForRoom = `${
|
|
ServerResponseChannels.RTC_RECEIVING_MASTER_ANSWER
|
|
}:${conversation._id.toString()}`;
|
|
|
|
$ws.on(someoneJoinedChannelNameForRoom, (res: RtcNewUserJoinedResponse) => {
|
|
toast.success('NEWBIE JOINED!!!');
|
|
console.log('someone calling me', res);
|
|
|
|
// todo, take trickle into account to not overwrite prev peer
|
|
|
|
const peerForMeAndNewbie = new Peer({
|
|
initiator: false,
|
|
stream: userLocalStream,
|
|
trickle: false, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times
|
|
config: {
|
|
iceServers,
|
|
},
|
|
});
|
|
|
|
peerForMeAndNewbie.signal(res.simplePeerSignal);
|
|
|
|
setConversationMap((draft) => {
|
|
if (draft[conversation._id.toString()]) {
|
|
draft[conversation._id.toString()].room = {
|
|
...(draft[conversation._id.toString()].room ?? {}),
|
|
[res.userWhoCalled]: { peer: peerForMeAndNewbie, isConnecting: true },
|
|
};
|
|
}
|
|
});
|
|
|
|
peerForMeAndNewbie.on('signal', (signal) => {
|
|
console.log('sending an answer to the slave', res);
|
|
|
|
$ws.emit(
|
|
ServerRequestChannels.RTC_ANSWER_SOMEONE_FOR_LINE,
|
|
new RtcAnswerSomeoneRequest(res.userWhoCalled, res.lineId, signal),
|
|
);
|
|
});
|
|
|
|
peerForMeAndNewbie.on('stream', (remoteStream: MediaStream) => {
|
|
// globally updating conversation so that other views can render what they want
|
|
toast.success('got stream from remote, going to add to our ');
|
|
|
|
console.log('got stream from newbie');
|
|
|
|
setConversationMap((draft) => {
|
|
if (draft[conversation._id.toString()].room[res.userWhoCalled]) {
|
|
draft[conversation._id.toString()].room[res.userWhoCalled].stream = remoteStream;
|
|
remoteStream.getTracks().forEach((track) => {
|
|
// TODO: add particular track to right place
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
peerForMeAndNewbie.on('connect', () => {
|
|
toast.success('successfully connected to another peer');
|
|
setConversationMap((draft) => {
|
|
if (draft[conversation._id.toString()].room[res.userWhoCalled]) {
|
|
draft[conversation._id.toString()].room[res.userWhoCalled].isConnecting = false;
|
|
}
|
|
});
|
|
});
|
|
|
|
peerForMeAndNewbie.on('track', (track, stream) => {
|
|
// TODO: add to room contents
|
|
|
|
toast('a peer added a track to a stream');
|
|
});
|
|
|
|
peerForMeAndNewbie.on('close', () => {
|
|
// the person will be removed from the tuned in list, but the connections here are decoupled from that flow
|
|
// we want to manage the room within the master conversation and remove it for ourselves
|
|
// !this is false, see unmount, we are relying on tuned list for disconnections
|
|
|
|
setConversationMap((draft) => {
|
|
if (draft[conversation._id.toString()].room[res.userWhoCalled]) {
|
|
draft[conversation._id.toString()].room[res.userWhoCalled].peer?.destroy();
|
|
delete draft[conversation._id.toString()].room[res.userWhoCalled];
|
|
}
|
|
});
|
|
|
|
console.error('peer connection was closed');
|
|
|
|
toast.error('peer connection was closed');
|
|
});
|
|
|
|
peerForMeAndNewbie.on('error', (err) => {
|
|
console.error(err);
|
|
toast.error('there was a problem with the peer connection');
|
|
});
|
|
});
|
|
|
|
$ws.on(mastersAnswerReceivedChannelNameForRoom, (res: RtcReceiveAnswerResponse) => {
|
|
toast.success('MASTER gave me an answer!!!');
|
|
|
|
console.log('master gave me this answer: ', res);
|
|
|
|
// find our local connection to this peer in the room contents
|
|
setConversationMap((draft) => {
|
|
const localPeerForMasterAndMe = draft[res.lineId]?.room[res.masterUserId];
|
|
|
|
if (localPeerForMasterAndMe) {
|
|
localPeerForMasterAndMe.peer.signal(res.simplePeerSignal);
|
|
}
|
|
});
|
|
});
|
|
|
|
return () => {
|
|
$ws.removeAllListeners(mastersAnswerReceivedChannelNameForRoom);
|
|
$ws.removeAllListeners(someoneJoinedChannelNameForRoom);
|
|
};
|
|
}, [userLocalStream]);
|
|
|
|
// find when someone leaves the room and destroy peer and remove them from our map
|
|
useEffect(() => {
|
|
// go through tuned in users
|
|
// if we have someone in room who is not in tuned in, then destroy + delete
|
|
if (conversation.room) {
|
|
Object.entries(conversation.room).forEach(([roomUserId, roomContents]) => {
|
|
if (!conversation.tunedInUsers.includes(roomUserId)) {
|
|
console.log('someone left the room!!!');
|
|
|
|
roomContents.peer.destroy();
|
|
|
|
setConversationMap((draft) => {
|
|
delete draft[conversation._id.toString()].room[roomUserId];
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}, [conversation.tunedInUsers, conversation.room]);
|
|
|
|
// go through all of the peers and destroy and update conversation map
|
|
useUnmount(() => {
|
|
setConversationMap((draft) => {
|
|
if (draft[conversation._id.toString()].room) {
|
|
Object.values(draft[conversation._id.toString()].room).forEach((roomPeerContents) => {
|
|
roomPeerContents.peer.destroy();
|
|
|
|
console.log('destroyed peer connection as I am leaving room');
|
|
});
|
|
}
|
|
|
|
delete draft[conversation._id.toString()].room;
|
|
});
|
|
});
|
|
|
|
// TODO: improved listener manager for each peer connection
|
|
const setPeerListeners = useCallback(
|
|
(peer) => {
|
|
//
|
|
},
|
|
[setConversationMap],
|
|
);
|
|
|
|
// share audio to foreground conversation | either push to talk or toggle broadcasting
|
|
const handleStartTalking = useCallback((record = false) => {
|
|
// if record, then start recording
|
|
// add stream if not there for this peer connection or just add track to stream
|
|
}, []);
|
|
|
|
const handleStopTalking = useCallback(() => {
|
|
// stop recording if was recording
|
|
// stop sharing audio | maybe stop track to avoid stereo distortion
|
|
}, []);
|
|
|
|
const handleShareVideo = useCallback(() => {
|
|
// add track and/or stream of video to the peer connections for currently selected room
|
|
}, []);
|
|
|
|
const handleShareScreen = useCallback(() => {
|
|
// add track and/or stream of screen to the peer connections for currently selected room
|
|
}, []);
|
|
|
|
// ============= END OF STREAMING MODULE ========
|
|
|
|
useEffect(() => {
|
|
// call all of the tuned in folks that are already here
|
|
console.log(conversation.tunedInUsers);
|
|
|
|
return () => {
|
|
// close peer connections here
|
|
// update the conversation map as well with proper states and data
|
|
};
|
|
}, []);
|
|
|
|
return null;
|
|
}
|
|
|
|
// ANOTHER APPROACH
|
|
// have one hook used in the upper level
|
|
// this hook renders multiple components for each room
|
|
// each component for each room returns data for that room
|
|
// this upper level rooms hook returns each room's data
|