import React, { useEffect, useState, useContext, useCallback } from 'react'; import useRooms from './RoomsProvider'; import useSockets from './SocketProvider'; import MasterLineData from '@nirvana/core/models/masterLineData.model'; import { ServerRequestChannels, ServerResponseChannels, SomeoneConnectedResponse, SomeoneDisconnectedResponse, SomeoneTunedResponse, SomeoneUntunedFromLineResponse, TuneToLineRequest, UserStartedBroadcastingResponse, UserStoppedBroadcastingResponse, ConnectToLineRequest, UntuneFromLineRequest, } from '@nirvana/core/sockets/channels'; import toast from 'react-hot-toast'; import { useImmer } from 'use-immer'; import { LineMemberState } from '@nirvana/core/models/line.model'; import { useAsyncFn } from 'react-use'; import { updateLineMemberState } from '../api/NirvanaApi'; import UpdateLineMemberState from '@nirvana/core/requests/updateLineMemberState.request'; import useAuth from './AuthProvider'; import { User } from '@nirvana/core/models/user.model'; type LineIdToMasterLine = { [lineId: string]: MasterLineData; }; // TODO: implement the below to save renders type TunedMembersMap = { [lineId: string]: string[]; }; type ConnectedMembesMap = { [lineId: string]: string[]; }; type UserMap = { [userId: string]: User; }; type BroadcastersMap = { [lineId: string]: string[]; }; interface IRealTimeRoomProvider { roomsMap: LineIdToMasterLine; selectedLineId?: string; handleSelectLine?: (newLineId: string) => void; handleUpdateLineMemberState?: (lineId: string, newState: LineMemberState) => void; } const RealTimeRoomContext = React.createContext({ roomsMap: {}, }); /** * * handles reads of new data * keeps listening to incoming socket events to make sure that the realtime rooms map is highly available * * on load, we want to grab all of the rooms we are in * put them in a map * * fetch more audio clips, fire off async function to fetch more and add to the room map * * Socket Rooms: * - all people online for a line * - all tuned in folks on a line...have it selected or toggle tuned * * Socket Events: * - someone connected * - someone tuned in * - someone started broadcasting * - someone stopped broadcasting * * - someone disconnected...take them out of the necessary lists * - someone left x room * - someone joined x room * * - someone added me to line * * - someone went into flow state their status * * Socket Emissions: * - join a line * - tune into a line * - send audio clip * - create a line -> send to specific people * * REST endpoints: * - toggle tune or untoggle tune * - fetch content blocks for line history - react query */ export function RealTimeRoomProvider({ children }: { children: React.ReactChild }) { const { rooms } = useRooms(); const { user } = useAuth(); const { $ws } = useSockets(); const [roomMap, updateRoomMap] = useImmer({}); const [selectedLineId, setSelectedLineId] = useState(); const [moveLineState, moveLine] = useAsyncFn(updateLineMemberState); /** All line listeners */ useEffect(() => { // when me or anyone just initially connects to line $ws.on(ServerResponseChannels.SOMEONE_CONNECTED_TO_LINE, (res: SomeoneConnectedResponse) => { console.log(`${res.userId} connected to room ${res.lineId}`); updateRoomMap((draft) => { if (!draft[res.lineId]) { toast.error('there was a problem updating rooms!!!'); return; } draft[res.lineId].connectedMemberIds = res.allUsers; }); }); // someone tuning in, including perhaps me | either toggled in or just temporary $ws.on(ServerResponseChannels.SOMEONE_TUNED_INTO_LINE, (res: SomeoneTunedResponse) => { console.log(`${res.userId} tuned into line ${res.lineId}`); updateRoomMap((draft) => { if (!draft[res.lineId]) { toast.error('there was a problem updating rooms!!!'); return; } draft[res.lineId].tunedInMemberIds = res.allUsers; }); }); $ws.on( ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE, (res: SomeoneUntunedFromLineResponse) => { console.log(`${res.userId} untuned from ${res.lineId}`); updateRoomMap((draft) => { if (!draft[res.lineId]) { toast.error('there was a problem updating rooms!!!'); return; } if (draft[res.lineId].tunedInMemberIds) { draft[res.lineId].tunedInMemberIds = draft[res.lineId].tunedInMemberIds.filter( (userId) => userId !== res.userId, ); } }); }, ); // remove them from the line connected list and tuned list if they are there $ws.on( ServerResponseChannels.SOMEONE_DISCONNECTED_FROM_LINE, (res: SomeoneDisconnectedResponse) => { updateRoomMap((draft) => { if (!draft[res.lineId]) { toast.error('there was a problem updating rooms!!!'); return; } draft[res.lineId].connectedMemberIds = draft[res.lineId].connectedMemberIds?.filter( (userId) => userId !== res.userId, ); draft[res.lineId].tunedInMemberIds = draft[res.lineId].tunedInMemberIds?.filter( (userId) => userId !== res.userId, ); }); }, ); $ws.on( ServerResponseChannels.SOMEONE_STARTED_BROADCASTING, (res: UserStartedBroadcastingResponse) => { console.log(`${res.userId} is starting to broadcast in ${res.lineId}`); updateRoomMap((draft) => { if (!draft[res.lineId]) { toast.error('there was a problem updating rooms!!!'); return; } if (draft[res.lineId].currentBroadcastersUserIds) { draft[res.lineId].currentBroadcastersUserIds.push(res.userId); } else { draft[res.lineId].currentBroadcastersUserIds = [res.userId]; } }); }, ); $ws.on( ServerResponseChannels.SOMEONE_STOPPED_BROADCASTING, (res: UserStoppedBroadcastingResponse) => { console.log(`${res.userId} is STOPPED BROADCASTING in ${res.lineId}`); updateRoomMap((draft) => { if (!draft[res.lineId]) { toast.error('there was a problem updating rooms!!!'); return; } if (draft[res.lineId].currentBroadcastersUserIds) { draft[res.lineId].currentBroadcastersUserIds = draft[ res.lineId ].currentBroadcastersUserIds.filter((userId) => userId !== res.userId); } }); }, ); return () => { // ?perhaps only remove specific ones? // !this will remove all listeners across the app and we want it to? $ws.removeAllListeners(); }; }, [$ws, updateRoomMap]); 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], ); // converts the initial rooms fetch to a map useEffect(() => { if (rooms.value?.data?.masterLines?.length > 0) { updateRoomMap((draft) => { rooms.value.data.masterLines.forEach((masterLine) => { const lineId = masterLine.lineDetails._id.toString(); draft[lineId] = masterLine; handleConnectToLine(lineId); if (masterLine.currentUserMember.state === LineMemberState.TUNED) { handleTuneIntoLine(lineId); } }); }); } }, [rooms.value, updateRoomMap, handleConnectToLine, handleTuneIntoLine]); // persist whether I want it toggle tuned or not const handleUpdateLineMemberState = useCallback( (lineId: string, newState: LineMemberState) => { moveLine(new UpdateLineMemberState(newState), lineId) .then((_res) => { updateRoomMap((draft) => { draft[lineId].currentUserMember.state = newState; }); }) .catch((error) => { toast.error('problem in updating line member state'); console.error(error); }); }, [moveLine, updateRoomMap], ); /** * selection globally * tune into socket room and tell everyone else */ const handleSelectLine = useCallback( (newLineIdToSelect: string) => { setSelectedLineId((prevLineId) => { if (newLineIdToSelect === prevLineId) { return prevLineId; } // untune from the last line if it was just a temporary tuned one if (roomMap[prevLineId]?.currentUserMember.state === LineMemberState.INBOX) { handleUntuneFromLine(prevLineId); } // tune in if not already tuned into this line if (!roomMap[newLineIdToSelect].tunedInMemberIds?.includes(user._id.toString())) { handleTuneIntoLine(newLineIdToSelect); } return newLineIdToSelect; }); }, [setSelectedLineId, handleUntuneFromLine, roomMap, user, handleTuneIntoLine], ); return ( {children} ); } export default function useRealTimeRooms() { return useContext(RealTimeRoomContext); }