From 5daeeb5e92c71f05115a21ab98f1dda1bba8b2dc Mon Sep 17 00:00:00 2001 From: talksik Date: Fri, 13 May 2022 07:19:04 -0500 Subject: [PATCH] good progress on the tree although everything broken, more structure and really should just start fresh --- .../desktop/src/providers/AuthProvider.tsx | 23 +- .../src/providers/RealTimeRoomProvider.tsx | 272 +++++++++++ .../desktop/src/providers/RoomsProvider.tsx | 32 ++ packages/desktop/src/tree/electronApp.tsx | 11 +- .../src/tree/protected/terminal/Terminal.tsx | 387 +++++++++++++++ .../terminal/lines/lineIcon/index.tsx | 68 +++ .../terminal/lines/lineRow.tsx/index.tsx | 457 ++++++++++++++++++ .../tree/protected/terminal/navbar/Navbar.tsx | 183 +++++++ .../components/lines/lineRow.tsx/index.tsx | 143 +++--- 9 files changed, 1486 insertions(+), 90 deletions(-) create mode 100644 packages/desktop/src/providers/RealTimeRoomProvider.tsx create mode 100644 packages/desktop/src/providers/RoomsProvider.tsx create mode 100644 packages/desktop/src/tree/protected/terminal/Terminal.tsx create mode 100644 packages/desktop/src/tree/protected/terminal/lines/lineIcon/index.tsx create mode 100644 packages/desktop/src/tree/protected/terminal/lines/lineRow.tsx/index.tsx create mode 100644 packages/desktop/src/tree/protected/terminal/navbar/Navbar.tsx diff --git a/packages/desktop/src/providers/AuthProvider.tsx b/packages/desktop/src/providers/AuthProvider.tsx index 125b947..11792ad 100644 --- a/packages/desktop/src/providers/AuthProvider.tsx +++ b/packages/desktop/src/providers/AuthProvider.tsx @@ -9,6 +9,7 @@ interface IAuthProvider { user?: User; jwtToken?: string; setJwtToken?: (jwtToken: string) => void; + handleLogout?: () => void; } const AuthContext = React.createContext({}); @@ -34,6 +35,21 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } }, [jwtToken, fetchUser]); + const handleLogout = useCallback(() => { + setJwtToken(undefined); + + // ?should this be set here? what's a better way of + NirvanaApi._jwtToken = null; + + fetchUser() + .then((response) => { + toast.success('authenticated user'); + }) + .catch(() => { + toast.error('logged out'); + }); + }, [fetchUser, setJwtToken]); + const handleSetJwtToken = useCallback( (newJwtToken: string) => { setJwtToken(newJwtToken); @@ -43,7 +59,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { return ( {children} diff --git a/packages/desktop/src/providers/RealTimeRoomProvider.tsx b/packages/desktop/src/providers/RealTimeRoomProvider.tsx new file mode 100644 index 0000000..0ffc3cb --- /dev/null +++ b/packages/desktop/src/providers/RealTimeRoomProvider.tsx @@ -0,0 +1,272 @@ +import React, { useEffect, useState, useCallback, useContext } from 'react'; +import useRooms from './RoomsProvider'; +import useSockets from './SocketProvider'; + +import MasterLineData from '@nirvana/core/models/masterLineData.model'; +import { LineMemberState } from '@nirvana/core/models/line.model'; + +import { + ConnectToLineRequest, + ServerRequestChannels, + ServerResponseChannels, + SomeoneConnectedResponse, + SomeoneTunedResponse, + SomeoneUntunedFromLineResponse, + StartBroadcastingRequest, + StopBroadcastingRequest, + TuneToLineRequest, + UntuneFromLineRequest, + UserStartedBroadcastingResponse, + UserStoppedBroadcastingResponse, +} from '@nirvana/core/sockets/channels'; + +type LineIdToMasterLine = { + [lineId: string]: MasterLineData; +}; + +interface IRealTimeRoomProvider { + roomsMap: LineIdToMasterLine; +} + +const RealTimeRoomContext = React.createContext({ roomsMap: {} }); + +export function RealTimeRoomProvider({ children }: { children: React.ReactChild }) { + const { rooms } = useRooms(); + const { $ws } = useSockets(); + const [realTimeRoomMap, setRealTimeRoomMap] = useState({}); + + useEffect(() => { + if (rooms.value) { + /** + * initiate listeners + */ + + /** + * TODO: P0 : notified that someone else added me to a line they created + */ + // 1. get the new line from axios or just from the ws itself + // 2. add it to the lines map with all of the right data + // 3. make sure to connect to it as if it's inbox material + + // when me or anyone just initially connects to line + $ws.on(ServerResponseChannels.SOMEONE_CONNECTED_TO_LINE, (res: SomeoneConnectedResponse) => { + console.log( + `connected to line...here are all of the updated in the conected line ${res.lineId}...this isn't reliable considering it's not updated later`, + res.allConnectedIntoUserIds, + ); + + setRealTimeRoomMap((prevLinesMap) => { + const newMap = { ...prevLinesMap }; + + if (newMap[res.lineId]) + newMap[res.lineId].connectedMemberIds = [ + ...(newMap[res.lineId]?.connectedMemberIds ?? []), + res.userId, + ]; + + return newMap; + }); + }); + + // someone tuning in, including perhaps me | either toggled in or just temporary + $ws.on(ServerResponseChannels.SOMEONE_TUNED_INTO_LINE, (res: SomeoneTunedResponse) => { + console.log(`here are all of updated users in the tuned in room`, res.allTunedIntoUserIds); + + // TODO: if toggled in, make sure to update the current line member in the lines map so that + // we can know to untune if user selects another line + + // below, we are setting the list of tuned in folks based on fresh list from the server + // better than just adding and removing? i think so, but have to handle not interrupting existing peer connections as this changes + setRealTimeRoomMap((prevLinesMap) => { + const newMap = { ...prevLinesMap }; + + if (newMap[res.lineId]) { + newMap[res.lineId].tunedInMemberIds = res.allTunedIntoUserIds; + + // if user is me, make sure to show me my updated line member association + if (newMap[res.lineId].currentUserMember?.userId.toString() === res.userId) { + newMap[res.lineId].currentUserMember.lastVisitDate = new Date(); + newMap[res.lineId].currentUserMember.state = res.toggledIn + ? LineMemberState.TUNED + : LineMemberState.INBOX; + } + + // TODO: update the relevant lineMember (based on which userId is given): state and last visit date if current user is joining + // and not just if it's the current user toggling in + // right now, we just want user to know number of folks tuned and no need to expose who is toggle tuned...that lineMember can be stale + } + + return newMap; + }); + }); + + $ws.on( + ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE, + (res: SomeoneUntunedFromLineResponse) => { + console.log( + `here are all of updated users in the tuned in room`, + res.allTunedIntoUserIds, + ); + + // TODO: if toggled in, make sure to update the current line member in the lines map so that + // we can know to untune if user selects another line + + setRealTimeRoomMap((prevLinesMap) => { + const newMap = { ...prevLinesMap }; + + // TODO: update the relevant lineMember (based on which userId is given): state and last visit date if current user is joining + + if (newMap[res.lineId]) newMap[res.lineId].tunedInMemberIds = res.allTunedIntoUserIds; + + return newMap; + }); + }, + ); + + $ws.on( + ServerResponseChannels.SOMEONE_STARTED_BROADCASTING, + (res: UserStartedBroadcastingResponse) => { + console.log('someone is starting to broadcast'); + + setRealTimeRoomMap((prevLinesMap) => { + const newMap = { ...prevLinesMap }; + + if (newMap[res.lineId]) + newMap[res.lineId].currentBroadcastersUserIds = [ + ...(newMap[res.lineId].currentBroadcastersUserIds ?? []), + res.userId, + ]; + + return newMap; + }); + }, + ); + + $ws.on( + ServerResponseChannels.SOMEONE_STOPPED_BROADCASTING, + (res: UserStoppedBroadcastingResponse) => { + setRealTimeRoomMap((prevLinesMap) => { + const newMap = { ...prevLinesMap }; + + if (newMap[res.lineId]?.currentBroadcastersUserIds) { + newMap[res.lineId].currentBroadcastersUserIds = newMap[ + res.lineId + ].currentBroadcastersUserIds.filter( + (broadcasterUserId) => broadcasterUserId !== res.userId, + ); + } + + return newMap; + }); + }, + ); + } + }, [rooms.value, $ws]); + + /** handlers for emitting events to server */ + const handleConnectToLine = useCallback( + (lineId: string) => { + $ws.emit(ServerRequestChannels.CONNECT_TO_LINE, new ConnectToLineRequest(lineId)); + }, + [$ws], + ); + + /** + * toggle into a specific line + * @param temporary: denotes whether we are just listening in or want to persist "toggling" it on so that it shows up in overlay + * TODO: have loading state for this particular part of context value + */ + const handleTuneToLine = useCallback( + (lineId: string, turnToggleOn = false) => { + // they already are in the socket room for updates including media connections and disconnections + // but set the flag so that the line row can know whether or not to start the webrtc process + // and know when to get out or disconnect from the webrtc when the flag turns off + + $ws.emit(ServerRequestChannels.TUNE_INTO_LINE, new TuneToLineRequest(lineId, turnToggleOn)); + }, + [$ws], + ); + + const handleUnTuneToLine = useCallback( + (lineId: string) => { + // they already are in the socket room for updates including media connections and disconnections + // but set the flag so that the line row can know whether or not to start the webrtc process + // and know when to get out or disconnect from the webrtc when the flag turns off + + $ws.emit(ServerRequestChannels.UNTUNE_FROM_LINE, new UntuneFromLineRequest(lineId)); + }, + [$ws], + ); + + /** + * This is when the user wants to tell everyone that they are streaming/broadcasting/buzzing to + * a specific line, whether they are toggle tuned or temporarily tuned in + * + * Note: They could be using push to talk or toggle broadcast + * + * Note: this is handling the data transmission as is the responsibility of this overall context provider + * and not necessarily the aspect of enabling and disabling streaming as webrtc is reliable enough for that + * + * @param lineId the line that the current user is talking into + */ + const handleStartBroadcast = useCallback( + (lineId: string) => { + // emit telling people + $ws.emit(ServerRequestChannels.BROADCAST_TO_LINE, new StartBroadcastingRequest(lineId)); + }, + [$ws], + ); + + const handleStopBroadcast = useCallback( + (lineId: string) => { + // emit telling people + $ws.emit(ServerRequestChannels.STOP_BROADCAST_TO_LINE, new StopBroadcastingRequest(lineId)); + + // ?handle recording here as well? or record the incoming stream instead? + // ?could just take in the audiochunks here and all, but not sure yet + }, + [$ws], + ); + + useEffect(() => { + if (rooms.value?.data?.masterLines?.length > 0) { + setRealTimeRoomMap((prevMappings) => { + // go through the lines from the persistent store + + // get all of the id's and map assign to the main object + + // ?prolly have no previous at this point...but I'm okay with override since this + // ?useeffect is triggered on the refetching of the persistent store so we + // ?are prolly looking to do a full app refresh and connection refresh + const newMap = { ...prevMappings }; + + rooms.value.data.masterLines.forEach((masterLine) => { + const lineId = masterLine.lineDetails._id.toString(); + newMap[lineId] = masterLine; + + handleConnectToLine(lineId); + + // tune into lines that I should be + if (masterLine.currentUserMember.state === LineMemberState.TUNED) { + // don't need to turn toggle on, it's already on + // ! CHANGE PARADIGM TO NOT INCLUDING TOGGLING IN REQUESTS WITH THE SAME AS TUNING IN + // right now, writing just because of later logic + handleTuneToLine(lineId, true); + } + }); + + return newMap; + }); + } + }, [rooms.value, handleConnectToLine, handleTuneToLine, setRealTimeRoomMap]); + + return ( + + {' '} + + ); +} + +export default function useRealTimeRooms() { + return useContext(RealTimeRoomContext); +} diff --git a/packages/desktop/src/providers/RoomsProvider.tsx b/packages/desktop/src/providers/RoomsProvider.tsx new file mode 100644 index 0000000..63ccc7f --- /dev/null +++ b/packages/desktop/src/providers/RoomsProvider.tsx @@ -0,0 +1,32 @@ +import React, { useContext } from 'react'; +import { useAsync } from 'react-use'; +import { getUserLines } from '../api/NirvanaApi'; +import { AsyncState } from 'react-use/lib/useAsyncFn'; +import GetUserLinesResponse from '@nirvana/core/responses/getUserLines.response'; +import NirvanaResponse from '@nirvana/core/responses/nirvanaResponse'; + +/** + * NOTE: lines, rooms, conversations are synonymous + */ + +interface IRoomProvider { + rooms?: AsyncState>; +} + +const RoomsContext = React.createContext({}); + +/** + * in charge of the main cach object for room data + * modified with real time socket data + */ +export function RoomsProvider({ children }: { children: React.ReactChild }) { + const rooms = useAsync(getUserLines); + + // TODO: refetch handler + + return {children}; +} + +export default function useRooms() { + return useContext(RoomsContext); +} diff --git a/packages/desktop/src/tree/electronApp.tsx b/packages/desktop/src/tree/electronApp.tsx index 141e52b..4a587c2 100644 --- a/packages/desktop/src/tree/electronApp.tsx +++ b/packages/desktop/src/tree/electronApp.tsx @@ -1,16 +1,25 @@ import React from 'react'; import { Toaster } from 'react-hot-toast'; +import { RoomsProvider } from '../providers/RoomsProvider'; +import { RealTimeRoomProvider } from '../providers/RealTimeRoomProvider'; import { AuthProvider } from '../providers/AuthProvider'; import { ElectronProvider } from '../providers/ElectronProvider'; import { SocketProvider } from '../providers/SocketProvider'; import ProtectedRoute from './protected/ProtectedRoute'; +import Terminal from './protected/terminal/Terminal'; export default function ElectronApp() { return ( - yo its connected dawg + + + + + + + diff --git a/packages/desktop/src/tree/protected/terminal/Terminal.tsx b/packages/desktop/src/tree/protected/terminal/Terminal.tsx new file mode 100644 index 0000000..0c63e82 --- /dev/null +++ b/packages/desktop/src/tree/protected/terminal/Terminal.tsx @@ -0,0 +1,387 @@ +import { Avatar, Skeleton, Tooltip } from 'antd'; +import React, { useCallback, useMemo, useState } from 'react'; +import useRealTimeRooms from '../../../providers/RealTimeRoomProvider'; +import useElectron from '../../../providers/ElectronProvider'; +import useAuth from '../../../providers/AuthProvider'; +import NavBar from './navbar/Navbar'; +import { FiActivity, FiHeadphones, FiSettings, FiSun } from 'react-icons/fi'; +import { LineMemberState } from '@nirvana/core/models/line.model'; +import MasterLineData from '@nirvana/core/models/masterLineData.model'; +import LineRow from './lines/lineRow.tsx'; +import useRooms from '../../../providers/RoomsProvider'; +import toast from 'react-hot-toast'; +import { FaPlus } from 'react-icons/fa'; +import LineIcon from './lines/lineIcon'; + +export default function Terminal() { + const { rooms: initialRoomsState } = useRooms(); + const { roomsMap } = useRealTimeRooms(); + const { desktopMode } = useElectron(); + + const { user } = useAuth(); + + const [selectedLine, setSelectedLine] = useState(); + + const allLines: MasterLineData[] = useMemo(() => { + const masterLines: MasterLineData[] = Object.values(roomsMap); + + // TODO: sort based on the audio blocks and currentMember lastActiveDate + + return masterLines.filter( + (masterLine) => masterLine.currentUserMember.state === LineMemberState.INBOX, + ); + }, [roomsMap]); + + const toggleTunedLines = useMemo(() => { + const masterLines: MasterLineData[] = Object.values(roomsMap); + + return masterLines.filter( + (masterLine) => masterLine.currentUserMember.state === LineMemberState.TUNED, + ); + }, [roomsMap]); + + /** show user line details on click of one line */ + const handleSelectLine = useCallback( + (newLineIdToSelect: string) => { + toast('selecting line!! NOT IMPLEMENTED'); + }, + [setSelectedLine], + ); + + return ( +
+ + +
+
+ {/* tuned in lines block */} +
+ {/* tuned in header + general controls */} + + +
+ + + +

Rooms

+ +

{`${toggleTunedLines?.length || 0}/3`}

+
+
+
+ + {/* list of toggle tuned lines */} +
+ {toggleTunedLines.map((masterLineData) => ( + { + console.log('asdf'); + }} + /> + ))} +
+
+ + {!(allLines.length > 0) && !(toggleTunedLines.length > 0) && ( + + You have no lines!
Create one to connect to your team instantly. +
+ )} + + {/* rest of the lines */} +
+ {initialRoomsState.loading ? ( + + ) : ( + allLines.map((masterLineData) => ( + + )) + )} +
+ +
+ +
+
+ + {selectedLine ? ( + + ) : ( +
+ {`Hi ${user.givenName}!`} + {"You're all set!"} +
+ )} +
+
+ ); +} + +function LineDetailsTerminal({ selectedLine }: { selectedLine: MasterLineData }) { + const { user } = useAuth(); + + console.log('selected line', selectedLine); + + const isUserToggleTuned = useMemo( + () => selectedLine?.currentUserMember?.state === LineMemberState.TUNED, + [selectedLine], + ); + + // seeing if I am in the list of broadcasters + // the source of truth from the socket connections telling me if my clicking actually made a round trip + const isUserBroadcasting = useMemo( + () => selectedLine?.currentBroadcastersUserIds?.includes(user._id.toString()), + [user, selectedLine], + ); + + // showing all tuned in members...they may not hear me since they might be doing something else + // but feeling of presentness + const tunedProfiles = useMemo(() => { + const pictureSources: { name: string; pictureSrc: string }[] = []; + + selectedLine.tunedInMemberIds?.forEach((tunedInMemberUserId) => { + // don't want to see my own picture + // TODO: don't show myself!? + if (tunedInMemberUserId === user._id.toString()) { + pictureSources.push(); + + pictureSources.push({ + name: user.givenName, + pictureSrc: user.picture, + }); + return; + } + + const otherUserObject = selectedLine.otherUserObjects?.find( + (userObj) => userObj._id.toString() === tunedInMemberUserId, + ); + if (otherUserObject?.picture) + pictureSources.push({ + name: otherUserObject.givenName, + pictureSrc: otherUserObject.picture, + }); + }); + + return pictureSources; + }, [selectedLine, user]); + + // pics for the line icons + const profilePictures = useMemo(() => { + const pictureSources: string[] = []; + + // ?don't add in my image as that's useless contextually? + // if (userData?.user?.picture) pictureSources.push(userData.user.picture); + + selectedLine.otherUserObjects?.forEach((otherUser) => { + if (otherUser.picture) pictureSources.push(otherUser.picture); + }); + + return pictureSources; + }, [selectedLine, user]); + + // TODO get from realtime provider to do this action + + const handleToggleTuneToLine = () => toast('NOT IMPLEMENTATED: toggle tuning'); + + return ( +
+ {/* line details */} +
+ {profilePictures && } + +
+ +

+ {selectedLine.lineDetails.name || selectedLine.otherUserObjects[0].givenName} +

+ + +
+ + + {`${ + selectedLine.otherMembers?.length + 1 ?? 0 + } members`} + + {`${ + selectedLine.tunedInMemberIds?.length ?? 0 + } in this room`} + +
+ + {/* TODO: move to on hover of line row */} + + + +
+ + {/* line timeline */} +
+ load more + + yesterday + +
+ {selectedLine.otherUserObjects.map((otherUser) => ( +
+ + {otherUser.givenName} + + {`${ + Math.floor(Math.random() * 10) + 1 + }:${Math.floor(Math.random() * 100) + 10}pm |`} + + {`${ + Math.floor(Math.random() * 60) + 1 + } seconds`} +
+ ))} + +
+ + {'Arjun Patel'} + + {`${ + Math.floor(Math.random() * 10) + 1 + }:${Math.floor(Math.random() * 100) + 10}pm |`} + + {`${ + Math.floor(Math.random() * 60) + 1 + } seconds`} +
+
+ + today + +
+ {selectedLine.otherUserObjects.map((otherUser) => ( + // TODO: show the shadow if it's unheard +
+ + {otherUser.name} + + {`${ + Math.floor(Math.random() * 10) + 1 + }:${Math.floor(Math.random() * 100) + 10}pm |`} + + {`${ + Math.floor(Math.random() * 60) + 1 + } seconds`} +
+ ))} +
+ + {/* live broadcasters */} + + + right now + +
+ {selectedLine.otherUserObjects.map((otherUser) => ( +
+ + {otherUser.name} + + + + +
+ ))} +
+
+ + +
+ ); +} diff --git a/packages/desktop/src/tree/protected/terminal/lines/lineIcon/index.tsx b/packages/desktop/src/tree/protected/terminal/lines/lineIcon/index.tsx new file mode 100644 index 0000000..517bd98 --- /dev/null +++ b/packages/desktop/src/tree/protected/terminal/lines/lineIcon/index.tsx @@ -0,0 +1,68 @@ +import { Avatar } from "antd"; +import React from "react"; +import { useMemo } from "react"; + +function LineIcon({ + sourceImages, + grayscale = true, +}: { + sourceImages: string[]; + grayscale: boolean; +}) { + const isMultiple = useMemo(() => { + if (sourceImages?.length > 1) return true; + + return false; + }, [sourceImages]); + + return ( + <> + {isMultiple ? ( +
+ {sourceImages.map((avatarSrc, index) => { + if (index === 0) { + return ( + + ); + } else if (index === 1) { + return ( + + ); + } + + // TODO: add third in the frame if we want later + return <>; + })} +
+ ) : ( + sourceImages?.map((avatarSrc, index) => ( + + )) + )} + + ); +} + +export default React.memo(LineIcon); diff --git a/packages/desktop/src/tree/protected/terminal/lines/lineRow.tsx/index.tsx b/packages/desktop/src/tree/protected/terminal/lines/lineRow.tsx/index.tsx new file mode 100644 index 0000000..2a89b9b --- /dev/null +++ b/packages/desktop/src/tree/protected/terminal/lines/lineRow.tsx/index.tsx @@ -0,0 +1,457 @@ +import { FiActivity, FiSun } from "react-icons/fi"; +import { + RtcAnswerRequest, + RtcCallRequest, + RtcNewUserResponse, + RtcReceiveAnswerResponse, + ServerResponseChannels, + SomeoneUntunedFromLineResponse, +} from "@nirvana/core/sockets/channels"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { $selectedLineId } from "../../../controller/recoil"; +import { Avatar } from "antd"; +import LineIcon from "../lineIcon"; +import { LineMemberState } from "@nirvana/core/models/line.model"; +import MasterLineData from "@nirvana/core/models/masterLineData.model"; +import Peer from "simple-peer"; +import { ServerRequestChannels } from "../../../../../core/sockets/channels"; +import moment from "moment"; +import toast from "react-hot-toast"; +import { useGetUserDetails } from "../../../controller/index"; +import { useLineDataProvider } from "../../../controller/lineDataProvider"; +import { useRecoilState } from "recoil"; + +// todo: send a much more comprehensive master line object? or just add properties to the +// masterLineData object so that we don't have different models to maintain between client and server + +export default function LineRow({ + masterLineData, + handleSelectLine, +}: { + masterLineData: MasterLineData; + handleSelectLine: (lineId: string) => void; +}) { + const [selectedLineId, setSelectedLineId] = useRecoilState($selectedLineId); + const { data: userData } = useGetUserDetails(); + + useEffect(() => { + console.warn("mounting linerow"); + + return () => console.warn("UNMOUNTING line row"); + }, []); + + // take the source of truth list of memeberIds tuned in, and see if I'm in it + const isUserTunedIn = useMemo( + () => + masterLineData.tunedInMemberIds?.includes(userData?.user?._id.toString()), + [masterLineData.tunedInMemberIds, userData] + ); + + /** + * TODO: slowly add to this and fix based on added features + * */ + const renderActivityIcon = useMemo(() => { + // if there is someone or me broadcasting here + if (masterLineData.currentBroadcastersUserIds?.length > 0) + return ; + + if (isUserTunedIn) + return ; + + // if there is new activity blocks for me + if (masterLineData.currentUserMember.lastVisitDate) + return ( + + ); + + return ( + + ); + }, [masterLineData, isUserTunedIn]); + + const renderRightActivity = useMemo(() => { + // TODO: get the profile pictures of the broadcasters + if (masterLineData.currentBroadcastersUserIds?.length > 0) + return ( + + {masterLineData.otherUserObjects?.map((otherUser, index) => ( + + ))} + + ); + + // if there is new activity/black dot, then show relative time as little bolder? or too much? + + // TODO: compare last visit date to latest audio block + if (false) + return ( + + {moment(masterLineData.currentUserMember.lastVisitDate).fromNow(true)} + + ); + + return ( + + {moment(masterLineData.currentUserMember.lastVisitDate).fromNow(true)} + + ); + }, [masterLineData]); + + const profilePictures = useMemo(() => { + const pictureSources: string[] = []; + + // ?don't add in my image as that's useless contextually? + // if (userData?.user?.picture) pictureSources.push(userData.user.picture); + + masterLineData.otherUserObjects?.forEach((otherUser) => { + if (otherUser.picture) pictureSources.push(otherUser.picture); + }); + + return pictureSources; + }, [masterLineData, userData]); + + return ( + <> +
+ handleSelectLine(masterLineData.lineDetails._id.toString()) + } + className={`flex flex-row items-center justify-start gap-2 p-2 px-4 h-14 hover:bg-gray-200 cursor-pointer transition-all + last:border-b-0 border-b border-b-gray-200 relative z-50 rounded ${ + selectedLineId === masterLineData.lineDetails._id.toString() && + "bg-gray-200 scale-110 shadow-2xl translate-x-3" + }`} + > + {/* status dot */} +
{renderActivityIcon}
+ + {profilePictures && ( + + )} + +

+ {masterLineData.lineDetails.name || + masterLineData.otherUserObjects[0].givenName} +

+ +
{renderRightActivity}
+
+ + {/* mounts and unmounts based on if in the room or now */} + {/* {isUserTunedIn && ( + + )} */} + + ); +} + +type PeerMap = { [userId: string]: Peer }; + +function StreamRoom({ + lineId, + tunedInUsers, + currentBroadcasters, +}: { + lineId: string; + tunedInUsers?: string[]; + currentBroadcasters?: string[]; +}) { + // ?could move this up the tree and pass it down? or set it in the header? + // local stream specifically for this stream room + const [localStream, setLocalStream] = useState(); + const userStreamTagRef = useRef(null); + const { data: userDetails } = useGetUserDetails(); + + // local peer map of userIds to peers + const [userPeers, setUserPeers] = useState({}); + + // ws listen to events of user disconnecting and such or rely on tunedin members prop + const { $ws } = useLineDataProvider(); + + // todo: get the right constraints based on user settings...audio, video, or none? header selections? make a decision + useEffect(() => { + if (userDetails) { + navigator.mediaDevices + .getUserMedia({ + video: false, + audio: { + echoCancellation: true, + autoGainControl: true, + }, + }) + .then((userStream) => { + setLocalStream(userStream); + + if (userStreamTagRef?.current) + userStreamTagRef.current.srcObject = userStream; + + // alternative to dom element + // const audio = new Audio(); + // audio.autoplay = true; + // audio.srcObject = userStream; + + // !TEST STUFF : removing distortion with headphones in + // test distortion to go away on disabling audio + // setTimeout(() => { + // console.log("stopping audio stream "); + + // userStream.getTracks().forEach((track) => { + // track.enabled = !track.enabled; + + // track.stop(); + // }); + // }, 0); + + // take the initial list of tunedInUsers without my own id + const everyOtherTunedUserId = tunedInUsers.filter( + (tunedUserId) => tunedUserId !== userDetails?.user._id.toString() + ); + + const localPeerConnections: PeerMap = {}; + + everyOtherTunedUserId.forEach((otherTunedInUserId) => { + // create local peer objects for them + const localPeerInitiator = new Peer({ + initiator: true, + stream: userStream, + trickle: false, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times + }); + + // set it for our map that we will iterate through to display streams in other child view + localPeerConnections[otherTunedInUserId] = localPeerInitiator; + + // notify each one with specific signal + localPeerInitiator.on("signal", (signal) => { + $ws.emit( + ServerRequestChannels.RTC_CALL_REQUEST, + new RtcCallRequest(lineId, otherTunedInUserId, signal) + ); + }); + }); + + setUserPeers(localPeerConnections); + + // answer calls + $ws.on( + `${ServerResponseChannels.RTC_NEW_USER_JOINED_RESPONSE_PREFIX}:${lineId}`, + (res: RtcNewUserResponse) => { + // create a local peer connection for this new user + + console.log( + "ooo newbie joined room, I guess I will accept it and send him my signal" + ); + console.log(res); + + var peerForMeAndNewbie = new Peer({ + initiator: false, + trickle: false, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times + stream: userStream, // add in my own stream that I got before + }); + + peerForMeAndNewbie.on("signal", (signal) => { + console.log( + "as the answerer, I am going to send back my signal so that the newbie can update his local peer for me" + ); + $ws.emit( + ServerRequestChannels.RTC_ANSWER_REQUEST, + new RtcAnswerRequest(lineId, res.newUserId, signal) + ); + }); + + peerForMeAndNewbie.signal(res.simplePeerSignal); + + setUserPeers((prevUsersPeers) => { + const newUserPeers = { ...prevUsersPeers }; + + newUserPeers[res.newUserId] = peerForMeAndNewbie; + + return newUserPeers; + }); + } + ); + + // take care of answers recevied + $ws.on( + `${ServerResponseChannels.RTC_RECEIVING_ANSWER_RESPONSE_PREFIX}:${lineId}`, + (res: RtcReceiveAnswerResponse) => { + console.log( + `oooo some master received my call and accepted it ${JSON.stringify( + res + )}` + ); + console.log(res); + + // find the peer we created earlier for this master + // ?is this okay? using the setter to get the current state? + setUserPeers((previousUserPeersMap) => { + const newUserPeerMap = { ...previousUserPeersMap }; + + console.log( + "here is the current peers map", + previousUserPeersMap + ); + + const peerForAnswerer = newUserPeerMap[res.answererUserId]; + + if (peerForAnswerer) { + peerForAnswerer.signal(res.simplePeerSignal); + } else { + console.error( + "could not find the peer we created before for this master" + ); + } + + // note needed to change state so that the peer object that gets iterated in dom isn't the old referenced one/we trigger refresh for the child component + return newUserPeerMap; + }); + } + ); + + $ws.on( + `${ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE}:${lineId}`, + (res: SomeoneUntunedFromLineResponse) => { + // find the peer object and remove from our userPeers map + // will also cause unmounting the child component which destroys peer object but also can destroy here + } + ); + }) + .catch((error) => { + console.error(error); + + toast.error( + "Make sure that you have permissions enabled and microphone connected" + ); + }); + } + + // TODO: p1 : destroy peers on load + // ! remove ws handlers so that the same channels don't get triggered twice when I retune into this line + return () => { + $ws.removeListener( + `${ServerResponseChannels.RTC_RECEIVING_ANSWER_RESPONSE_PREFIX}:${lineId}` + ); + $ws.removeListener( + `${ServerResponseChannels.RTC_NEW_USER_JOINED_RESPONSE_PREFIX}:${lineId}` + ); + }; + }, [userDetails, setUserPeers]); + + // calculate diff to clean our userPeerMap to unmount and detroy certain peer connections + useEffect(() => {}, [tunedInUsers]); + + useEffect(() => { + console.log("keeping an eye on user peers map"); + console.log(userPeers); + }, [userPeers]); + + // todo, someone tell the main object that I am finally connected after everything...different than tuned in + + // TODO: p1...when the peer map user count > tunedIn.length, then we get rid of the right person from list cuzz they have officially left or disconnected + useEffect(() => { + console.log("change in tuned in users in the streaming room!!!"); + + setUserPeers((prevUserPeersMap) => { + // go through the userIds here + // if tunedIn users doesn't have a userId, this guy prolly disconnected + + const newMap = { ...prevUserPeersMap }; //ensures going through the list of peers again to remove specific ones + + const otherUserIdsPeers = Object.keys(prevUserPeersMap); + + otherUserIdsPeers.forEach((otherUserId) => { + // problem if we are trying to show stream of someone who is not tuned in + if (!tunedInUsers.includes(otherUserId)) { + console.log("user left with id:", otherUserId); + const disconnectedLocalPeer = newMap[otherUserId]; + + if (disconnectedLocalPeer) { + disconnectedLocalPeer.peer?.destroy(); + delete newMap[otherUserId]; + } + } + }); + + return newMap; + }); + }, [tunedInUsers]); + + // todo: when user isUserBroadcasting is false, disable localUserStream in this component + // and also emit, which should already have been done + + // we only loop through peers that are associated to user Ids which exist in the currentBroadcasters array + return ( + <> +